| title | Packaging |
|---|---|
| description | Build installable Rust and TSX apps with nsh package for iOS devices, the iOS simulator, Android, macOS, Windows, Linux and the web, with app metadata, icons and signing. |
| order | 19 |
| group | platforms |
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. It needs no Xcode project and no Gradle.
- A Rust app (
nsh.jsonnames its"root") is packaged from its directory.nshbuilds the target's runner in release mode with your crate compiled in and the root mounted, with no QuickJS: Cargo, the Rust target of the platform (rustup target add …), and for Android the NDK. - A TSX app names its entry. Its release build (minified, compiled to QuickJS bytecode, exactly as
nsh build --releasebuilds it) goes next to the prebuilt runtime that ships with NSH, so it needs no Rust toolchain. An app with a crate of its own ("rust"without"root") gets runners built with its crate.
# A Rust app, from its directory
nsh package --target ios-simulator --run # build, install and launch in the simulator
nsh package --target android --run # build, install and launch on a device or emulator
nsh package --target macos --run # build and launch on this Mac
nsh package --target linux --run # build and launch on this machine
nsh package --target windows --run # build and launch on this machine
nsh package --target web --run # build, serve on this machine and open in the browser
nsh package --target ios --sign "Apple Development: Jane Doe (ABCDE12345)" --profile Dev.mobileprovision
# A TSX app: the same with its entry
nsh package App.tsx --target ios-simulator --runOutput goes to .nsh/out/ unless you pass --out. The sections below give each target's command for a Rust app; a
TSX app adds its entry (nsh package App.tsx --target …).
Note
To put NSH inside an existing native app instead, see Platforms, and for a GPUI or GPUI Kit app GPUI. To ship updates that apps download and verify, see Signed packages.
nsh package reads nsh.json in the app's directory (next to the entry file of a TSX app). 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 | required | The bundle identifier / Android package name, in reverse-DNS form under a domain you own. It also names where the app keeps its storage, so each app has its own. nsh package and nsh publish warn about the com.example. placeholder. |
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 also holds rust and root (the app's crates and its root component, Building with Rust),
theme and appearance, tailwind (see Tailwind) and capabilities (see
Sandbox and capabilities); Project structure lists
every key. Unknown keys are an error.
Every key above can be overridden on the command line, which is handy in CI:
nsh package --target android --app-version 1.4.0 --build 42| Flag | Description |
|---|---|
--target <ios | ios-simulator | android | macos | linux | windows | web> |
What to build for (required). |
-o, --out <dir> |
Output directory. Default .nsh/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. |
--abi <abi> |
Android: the ABIs of the APK, repeated or separated by commas (default: arm64-v8a and armeabi-v7a). |
--run |
Install and launch afterwards: in the iOS simulator, or on the Android device or emulator adb sees; macOS, Linux and Windows apps are launched on this machine; a web site is served on this machine and opened in the browser. Not for --target ios. |
nsh package --target ios-simulator --runWrites .nsh/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.
nsh package --target ios \
--sign "Apple Distribution: Example Inc (ABCDE12345)" \
--profile AppStore.mobileprovisionWrites .nsh/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 codesigningWithout --sign and --profile, NSH writes an unsigned .nsh/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.
nsh package --target android --runWrites .nsh/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.
- For a Rust app, the Android NDK (
sdkmanager "ndk;<version>", orANDROID_NDK_HOME), which builds itslibnsh.so.
The app targets API 35 and needs API 28 (Android 9) or newer. It contains native code for the ABIs of phones,
arm64-v8a and armeabi-v7a; with --run, also the device's, such as an emulator's x86_64. --abi picks them
yourself, for example --abi arm64-v8a for 64-bit phones only, the smallest APK:
nsh package --target android --abi arm64-v8aWithout --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 --target android \
--keystore release.jks --key-alias uploadPrefer the environment variable over --keystore-password so the password does not end up in your shell history.
nsh package --target macos --runWrites .nsh/out/<Name>.app: the executable in Contents/MacOS/, and nsh/assets/…, nsh/app.json (the window
title and the app's appearance) and a TSX app's nsh/bundle.nshc 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.
nsh package --target linux --runWrites .nsh/out/<Name>/ (the executable <Name>, nsh/assets/…, nsh/app.json with the window title and the
application id, a TSX app's nsh/bundle.nshc, <id>.desktop and the icon <id>.png) and the same as
.nsh/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.
nsh package --target windows --runWrites .nsh\out\<Name>\ (the executable <Name>.exe with the app's icon, nsh\assets\…, nsh\app.json with the
window title, the app id, the version and the appearance, and a TSX app's nsh\bundle.nshc) and the same as
.nsh\out\<Name>.zip. A TSX app needs no tools; a Rust app's runner is built with Visual Studio Build Tools. 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.
nsh package --target web --runWrites .nsh/out/<Name>/, a static site: index.html, manifest.webmanifest, the page script (nsh.<digest>.js)
and nsh.<digest>.wasm (a Rust app's engine and crate; for a TSX app the runtime, next to the app's ES module
app.<digest>.js), 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/ for you to open. Cache headers and embedding: Web.
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.
A Rust app's package is its runner, built with your crate and its root mounted, and the assets its crates register
(assets!()). A TSX app's package holds its bytecode, its capabilities.json (when it declares capabilities) and the
assets it imports, next to the prebuilt runtime:
| Target | Runtime | A TSX app |
|---|---|---|
| iOS | the nsh-ios-app executable |
<Name>.app/nsh/bundle.nshc, nsh/assets/… |
| Android | classes.dex, lib/<abi>/libnsh.so (without symbols, 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\… |
| Web | nsh.<digest>.wasm, nsh.<digest>.js |
<Name>/app.<digest>.js, assets/…, index.html |
A packaged app registers no native components; for those, embed NSH in your own app.
A Rust app builds its own runners. For TSX apps, 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) and nsh-windows-app (on Windows) 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); 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, ...).
- CLI: every command
- Signed packages: updates apps download
- Project structure:
nsh.jsonand the files of an app