Develop

Horizon OS NSDK project setup

Updated: May 13, 2026
The Horizon OS Native Software Development Kit (NSDK) lets you call Horizon OS platform APIs from native C and C++ code. It’s distributed as an AAR package containing:
  • Header files: C API declarations to include at compile time, such as <horizonos/versioning.h> and <horizonos/common.h>.
  • Stub shared library (libhzos.meta.so): a minimal shared object that provides symbol definitions for the linker at build time. It contains no real implementation.
At runtime, the operating system on the device supplies the real libhzos.meta.so with full implementations. The stub is never packaged into your APK. It exists only so the build system can resolve symbols during linking.

Prerequisites

  • Android Studio Ladybug (2024.2) or newer
  • Android Gradle Plugin 8.9 or higher
  • CMake 3.22.1 or higher (installable through Android Studio SDK Manager)
    • To install: Go to Tools > SDK Manager > SDK Tools and check CMake.
  • Android NDK (installable through Android Studio SDK Manager)
    • To Install: In the same SDK Tools tab, check NDK (Side by side).
  • Gradle 8.13 or higher
  • A physical Meta Quest device running HzOS.

Project setup

This guide walks through setting up a new Android Studio project that uses the Horizon OS NSDK. You have two options:
  • Option A: Gradle dependency (recommended). The Horizon OS NSDK AAR is published on Maven Central with Prefab metadata. Gradle automatically exposes the native headers and stub library to CMake. This is the simplest path with fewer manual steps.
  • Option B: Manual setup. Download the AAR, extract headers and the stub library into a local directory, and configure CMake by hand. Use this approach for offline development or when you need to customize the library layout.
Both options produce the same result: your native code links against the Horizon OS NSDK stub at build time and calls the real library on the device at runtime.
Note that the Manual Setup steps can easily be adapted to work with a project that uses CMake directly, without Gradle.

Step 1: Create a new Android Studio project

  1. Open Android Studio and select File > New > New Project.
  2. Under the Phone and Tablet category, choose Empty Activity. Quest devices run Android, so you can use standard Android templates. Later steps walk you through adding native C++ and CMake configuration.
  3. Set:
    • Language: Kotlin (or Java)
    • Minimum SDK: API 34
    • Build configuration language: Kotlin DSL (build.gradle.kts)
  4. Click Finish.
  5. Note your project name and package name. You’ll use them in the CMake configuration and JNI function signatures in later steps. This guide uses myapp and com.example.myapp as examples; replace them with your actual values.
  6. Once the project is created, follow Option A or Option B below to integrate the HzOS NSDK.

Step A1: Add the Maven Central repository

In your project’s settings.gradle.kts, make sure mavenCentral() is listed under dependencyResolutionManagement.repositories:
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()   // Required for Horizon OS NSDK
    }
}

Step A2: Add the Horizon OS NSDK dependency

In your version catalog (gradle/libs.versions.toml), add the Horizon OS NSDK library:
[versions]
horizon-os-nsdk = "204"    # Replace with latest Horizon OS NSDK version

[libraries]
horizon-os-nsdk = { group = "com.meta.horizonos", name = "horizon-os-nsdk", version.ref = "horizon-os-nsdk" }
Then in app/build.gradle.kts, add the dependency:
dependencies {
    implementation(libs.horizon.os.nsdk)
}

Step A3: Enable Prefab and configure the build

Update your app/build.gradle.kts to enable Prefab, set the ABI filter, and exclude the stub from the APK:
android {
    // ... existing config ...

    buildFeatures {
        prefab = true
    }

    defaultConfig {
        // Only arm64-v8a is supported on Meta Quest devices.
        ndk {
            abiFilters.clear()
            abiFilters += listOf("arm64-v8a")
        }
    }

    // Exclude the stub .so from the APK. The real library is on the device.
    packaging {
        jniLibs {
            excludes += listOf("**/libhzos.meta.so")
        }
    }

    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
}
Create or update app/src/main/cpp/CMakeLists.txt. With Prefab, you use find_package instead of manually declaring an imported library:
cmake_minimum_required(VERSION 3.22.1)
project("myapp")

# Prefab exposes the Horizon OS NSDK package automatically.
find_package(horizon-os-nsdk REQUIRED CONFIG)

add_library(${CMAKE_PROJECT_NAME} SHARED
    nsdk-example.cpp
)

target_link_libraries(${CMAKE_PROJECT_NAME}
    android
    log
    horizon-os-nsdk::hzos
)
Note: You don’t need to create a separate horizon_os_nsdk_lib/ directory, write a CMake file for the imported library, or manually set include directories. Prefab handles all of this through the Gradle dependency.

Option B: Manual setup

Use this approach if you need to work offline, customize the library layout, or can’t use Gradle’s Prefab integration.
Download the latest Horizon OS NSDK AAR from Maven Central or your artifact repository.
  1. Configure the Maven Central repository in your project as described in Step A1: Add the Maven Central repository.
  2. Declare the Horizon OS NSDK dependency as described in Step A2: Add the Horizon OS NSDK dependency.
  3. Copy the .aar file to your project. Rename the extension from .aar to .zip and extract it to get the headers and stub library.

Step B1: Add the Horizon OS NSDK library to your project

Create a directory called horizon_os_nsdk_lib/ at the root of your project. This directory contains the NSDK headers and stub library. The structure should be:
horizon_os_nsdk_lib/
├── CMakeLists.txt
├── include/
│   └── horizonos/
│       ├── common.h
│       └── versioning.h
│       └── ...
└── stubs/
    └── arm64-v8a/
        └── libhzos.meta.so    (stub, build-time only)
After renaming and decompressing the AAR file, copy the relevant contents into horizon_os_nsdk_lib/.
Note: The AAR’s internal layout doesn’t match the directory structure above. After extracting, create the include/ and stubs/ directories manually, then copy the headers from prefab/modules/hzos_headers/include/ and the stub library from prefab/modules/hzos/libs/android.arm64-v8a/ into the corresponding locations. Rename android.arm64-v8a to arm64-v8a when creating the stubs/ subdirectory. The final structure must match the layout shown above.
Create horizon_os_nsdk_lib/CMakeLists.txt with the following content:
cmake_minimum_required(VERSION 3.22.1)
project("horizon_os_nsdk")

# Declare the Horizon OS NSDK stub as an imported shared library.
add_library(hzos.meta SHARED IMPORTED GLOBAL)

# Point to the ABI-specific stub.
set_target_properties(hzos.meta PROPERTIES
    IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/stubs/${ANDROID_ABI}/libhzos.meta.so
)

# Expose the NSDK headers to consumers.
# SYSTEM suppresses compiler warnings originating from these headers.
target_include_directories(hzos.meta SYSTEM
    INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include
)

Step B2: Configure CMake for your app module

Create or update app/src/main/cpp/CMakeLists.txt:
cmake_minimum_required(VERSION 3.22.1)
project("myapp")

# Import the Horizon OS NSDK library sub-project.
# Path is relative to this CMakeLists.txt (app/src/main/cpp/).
add_subdirectory(../../../../horizon_os_nsdk_lib horizon_os_nsdk_lib_binary)

add_library(${CMAKE_PROJECT_NAME} SHARED
    nsdk-example.cpp
)

target_link_libraries(${CMAKE_PROJECT_NAME}
    android
    log
    hzos.meta
)
Note: The ../../../../horizon_os_nsdk_lib path is relative to the location of this CMakeLists.txt file (app/src/main/cpp/). If you place your CMakeLists.txt at a different directory depth, adjust the relative path accordingly.

Step B3: Configure the app-level build.gradle.kts

Update your app/build.gradle.kts to enable CMake and exclude the stub from the APK:
android {
    // ... existing config ...

    defaultConfig {
        // Only arm64-v8a is supported on Meta Quest devices.
        ndk {
            abiFilters.clear()
            abiFilters += listOf("arm64-v8a")
        }
    }

    // Exclude the stub .so from the APK. The real library is on the device.
    packaging {
        jniLibs {
            excludes += listOf("**/libhzos.meta.so")
        }
    }

    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
}
Once CMake and Gradle are configured, follow the Common Steps to declare the HzOS SDK version, write native code, and build/run on a Quest device.

Common steps (both options)

Declare HzOS SDK version in your AndroidManifest.xml

Add the horizonos XML namespace and declare your HzOS SDK version requirements in app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:horizonos="http://schemas.horizonos/sdk"
    xmlns:tools="http://schemas.android.com/tools">

    <horizonos:uses-horizonos-sdk
        horizonos:minSdkVersion="66"
        horizonos:targetSdkVersion="204" />

    <application ...>
        <!-- your activities -->
    </application>
</manifest>
The minSdkVersion declares the lowest HzOS version your app supports. The targetSdkVersion declares the version your app was designed and tested against. These are HzOS version numbers, not Android API levels. Check the Horizon OS NSDK release notes for supported values. See the Horizon OS SDK versioning documentation for more information.
Verifying your device’s HzOS version: On your Quest device, go to Settings > General > Software Update to see the current OS version. You can also query it programmatically through ADB:
adb shell getprop ro.horizonos.version.sdk
Make sure the reported version meets or exceeds your minSdkVersion. If the property isn’t available on your device firmware, check the OS build number in Settings > System > About and cross-reference with the HzOS version mapping in the NSDK release notes.
Note on the horizonos XML namespace: The <horizonos:uses-horizonos-sdk> element is a custom manifest entry consumed by the Meta Horizon Store at app upload time for distribution targeting, and inspected by the platform at install time for diagnostics. It is not enforced at runtime — a sideloaded APK that omits it still installs and runs. Standard Android build tooling (Android Gradle Plugin and AAPT2) preserves it in the merged manifest but may emit an unknown-namespace warning during build, which is safe to ignore. To verify it’s present in your final APK, inspect the merged manifest:
# After building, check the merged manifest:
cat app/build/intermediates/merged_manifests/debug/AndroidManifest.xml | grep horizonos

Write native code

Create your C++ source file (such as app/src/main/cpp/nsdk-example.cpp):
#include <jni.h>
#include <string>
#include <horizonos/versioning.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_getHzOSVersion(
        JNIEnv* env,
        jobject /* this */) {
    std::string version = std::to_string(HzOS_getVersion());
    return env->NewStringUTF(version.c_str());
}
Note: The JNI function name must match your app’s package name. Replace com_example_myapp with your actual package, converting dots to underscores (for example, package com.mycompany.xrapp becomes Java_com_mycompany_xrapp_MainActivity_getHzOSVersion).
Load the library and display the version from your Kotlin activity. The default Empty Activity template uses Jetpack Compose, so you can pass the version string to the generated Greeting composable:
class MainActivity : ComponentActivity() {
    external fun getHzOSVersion(): String

    companion object {
        init {
            System.loadLibrary("myapp")
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val version = getHzOSVersion()
        enableEdgeToEdge()
        setContent {
            MyappTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "HzOS Version: $version",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

Build and run

Connect a Meta Quest device through USB or use adb connect over Wi-Fi. Select your Quest device as a target in Android Studio. Click Run or use ./gradlew assembleDebug from the terminal. The app installs and launches on the device.

Troubleshooting

CMake can’t find horizon-os-nsdk package (Option A only)

If CMake reports that it can’t find the horizon-os-nsdk package when using the Gradle/Prefab approach:
  • Make sure prefab = true is set under buildFeatures in your app/build.gradle.kts.
  • Run File > Sync Project with Gradle Files and wait for sync to complete before building.
  • Verify that the dependency version in libs.versions.toml matches an available release on Maven Central.
  • Check that mavenCentral() is listed in your settings.gradle.kts repositories.

Build fails with “undefined reference to HzOS_...”

The linker can’t find HzOS symbols. Check that:
  • Option A:find_package(horizon-os-nsdk REQUIRED CONFIG) is present in your CMakeLists.txt and you are linking against horizon-os-nsdk::hzos (not bare hzos). The headers are included transitively.
  • Option B:horizon_os_nsdk_lib/CMakeLists.txt exists and correctly declares the hzos.meta imported library. The stub .so is present at horizon_os_nsdk_lib/stubs/arm64-v8a/libhzos.meta.so. Your app’s CMakeLists.txt includes add_subdirectory for horizon_os_nsdk_lib and lists hzos.meta in target_link_libraries.

Build fails with “No such file or directory: horizonos/versioning.h”

The compiler can’t find the Horizon OS NSDK headers. Verify that:
  • Option A: Gradle sync completed successfully and prefab = true is enabled. The find_package call automatically sets up include paths.
  • Option B: The headers exist under horizon_os_nsdk_lib/include/horizonos/. The target_include_directories in horizon_os_nsdk_lib/CMakeLists.txt points to the correct path.

App crashes at runtime with UnsatisfiedLinkError

This typically means the native library failed to load. Common causes include:
  • Missing System.loadLibrary call: Make sure your activity’s companion object or static block loads the library.
  • Wrong library name: The name passed to System.loadLibrary must match the project() name in your app’s CMakeLists.txt (without the lib prefix and .so suffix).
  • ABI mismatch: Make sure abiFilters is set to arm64-v8a and you’re running on a real Quest device (not an x86 emulator).

App crashes with “cannot locate symbol HzOS_...”

The stub .so was accidentally packaged into the APK, or the device doesn’t have the required HzOS version. Check that:
  • packaging.jniLibs.excludes includes "**/libhzos.meta.so" in your build.gradle.kts.
  • The device’s HzOS version meets or exceeds the value of horizonos:minSdkVersion declared in your app’s AndroidManifest.xml.
  • <horizonos:uses-horizonos-sdk> is declared in your manifest. Without it, the Meta Horizon Store may ship your binary to devices that don’t meet your minimum HzOS version, producing this symptom in the field. Sideloaded builds are unaffected — the element is not enforced at install or runtime.

Gradle sync fails or CMake is not found

  • Install CMake 3.22.1 or higher through Tools > SDK Manager > SDK Tools > CMake.
  • Make sure the cmake.version in your build.gradle.kts matches an installed version.

Build stalls during NDK download

Gradle may automatically download and install the Android NDK if one isn’t already present. This is a large package (around 1.5 GB) and Gradle doesn’t show download progress, so the build can appear frozen. To avoid this, either wait for the download to complete (check for network or disk activity) or install the NDK manually through Tools > SDK Manager > SDK Tools before building. You can also pin a specific NDK version in app/build.gradle.kts:
android {
    ndkVersion = "27.0.12077973"   // Use an NDK version already installed
}

Only arm64-v8a is supported

Meta Quest devices exclusively use 64-bit ARM processors. Don’t add other ABIs, such as armeabi-v7a or x86_64, to abiFilters. They fail to link against the stub and aren’t supported.