---
title: Deploy to mobile
sidebar_label: 6. Deploy to mobile
description: Cross-compile the part 4 program for Android, push it over adb, and run it on the phone's GPU. The code does not change.
---

**Your code does not change.** The program from [part 4 of the series](04-your-first-pipeline.mdx), byte for byte, compiled for Android and pushed to a phone, prints the same numbers there that it printed on your workstation, on the phone's GPU when it has one and on its CPU otherwise. This page is the complete path: cross-compile, push, run, and the few lines of Kotlin an app adds around it.

You need the bundle (its `android-arm64` dist ships the CPU and Vulkan backends), the Android NDK, and a phone with USB debugging enabled.

## 1. Cross-compile

Same project, same `main.cpp`. The configure line adds the NDK's toolchain file and the ABI; the bundle's CMake package selects the Android dist on its own:

```bash
cmake -S . -B build-android \
      -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
      -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 \
      -DClikaRT_DIR="$CLIKART_BUNDLE_DIR/cmake"
cmake --build build-android
```

```text
-- ClikaRT 0.6.4: dist android-arm64 (backends: cpu;vulkan)
```

## 2. Push and run

The binary plus the two libraries from the Android dist go to the device; the phone needs nothing else installed:

```bash
adb shell mkdir -p /data/local/tmp/clikart
adb push build-android/hello \
    "$CLIKART_BUNDLE_DIR/lib/libClikaRT.so" \
    "$CLIKART_BUNDLE_DIR/lib/libClikaRT_vulkan.so" \
    /data/local/tmp/clikart/
adb shell "cd /data/local/tmp/clikart && chmod +x hello && \
    CLIKA_RT_LICENSE=CLIKA1-... LD_LIBRARY_PATH=. ./hello"
```

The program runs compute, so it needs a license credential like any other ClikaRT program, and a binary under `adb shell` reads it from the environment of the shell that starts it ([Get ClikaRT](../get-clikart.mdx#license-credential)). Push the credential to a file on the device and give `CLIKA_RT_LICENSE` that path when you would rather keep it off the command line.

On a Galaxy S24 Ultra:

```text
y = Tensor(shape=[2, 4], dtype=Float32, device=Vulkan:0, numel=8, data=[4.25, 4.25, 4.25, 4.25, 4.25, 4.25, ...])
row means = [4.25, 4.25] (expected 8*1*0.5 + 0.25 = 4.25)
```

The workstation printed `device=CUDA:0`; the phone prints `device=Vulkan:0`. Same program, same numbers. `pick_device()` from [part 3](03-devices-and-async.mdx) found the phone's GPU the same way it found the workstation's; on this phone the part 3 discovery loop lists:

```text
CPU     0: ARM
Vulkan  0: Adreno (TM) 750
```

## 3. From an app: a few lines of Kotlin

ClikaRT is a C++ library, and an Android app reaches it through the app's own native code. Kotlin loads your library and calls your function; ClikaRT stays on the native side:

```kotlin
object Pipeline {
    init { System.loadLibrary("pipeline") }
    external fun run(): String
}
```

```cpp title="pipeline_jni.cpp"
#include <jni.h>

#include <ClikaRT/clika_rt.h>

using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_Pipeline_run(JNIEnv* env, jobject) {
    const Tensor x = Tensor::ones({2, 8}, DataType::Float32);
    const Tensor w = Tensor::full({4, 8}, 0.5, DataType::Float32);
    const Tensor y = ops::relu(ops::linear(x, w));
    return env->NewStringUTF(y.to_string().c_str());
}
```

The CMake side adds one target next to `hello`:

```cmake
add_library(pipeline SHARED pipeline_jni.cpp)
target_link_libraries(pipeline PRIVATE ClikaRT::ClikaRT)
```

Beneath the JNI boundary the native side is the C++ library you built for arm64. Kotlin code can also skip the custom library entirely: the `io.clika:clika-runtime` artifact ships its own JNI bridge and every runtime library, and calls the runtime directly. Python stays on the server and desktop side.

An app has no shell to export a variable from and no per-user license file, so the credential is an argument instead. `ClikaRtAndroid.load(context, license = "CLIKA1-...")` places it for the process before the binding loads. A credential given there replaces one already in the process environment, and the default, `null`, leaves the environment as it is. Ship the credential the way you ship any other secret your app needs at start-up.

What the app ships beside the library, the build levels it declares (`compileSdk` 36, `minSdk` 28, the Android Gradle plugin 9.3.2 on gradle 9.5 or newer), the model library's own `libClikaRT_modelverse.so` when the app runs a model from the library, and the two facts it places before the first call (the CPU worker count `CLIKA_RT_NUM_THREADS`, read once before the first compute, and the cache root) are on [Package ClikaRT in an Android app](../../how-to/package-clikart-in-an-android-app.mdx).

Every platform works this way: one bundle, one toolchain file, the same code. A Jetson needs no cross-compile at all: it is an arm64 Linux machine and runs the linux-arm64 build directly. The [system requirements](/system-requirements.md) table lists the platforms and their backends. The tutorial ends here; [what to read next](../next-steps.md).
