Llim.run

Build with Gradle

The CLI syncs your source to a Gradle sandbox, runs the project's own Gradle wrapper remotely, and streams the log back. Install the APK on a cloud emulator, or sign an AAB and publish it to Google Play.

The CLI covers the whole surface. The TypeScript SDK provisions and builds too, but its builds pass signing material explicitly because the escrowed release key (--sign) is CLI-only:

SurfaceCLITypeScriptPythonGo
Provision a Gradle sandbox✓✓via RESTvia REST
Sync source and run Gradle✓✓not in SDKnot in SDK
Escrowed release signing (--sign)✓pass the material yourselfnot in SDKnot in SDK

The SDK capability matrix covers every other surface.

Build a project

One command does everything. It creates a Gradle sandbox (or reuses the one remembered for your git worktree), syncs the current directory, and runs assembleDebug with the project's Gradle wrapper:

lim gradle build .

Pick other tasks with --task; repeat it for several:

lim gradle build . --task :app:assembleRelease

Bare React Native repositories usually need nothing extra: the sandbox finds android/ on its own. If the Gradle root is nested and auto-discovery is ambiguous, point at it with --project-path:

lim gradle build . --project-path android

The sync skips paths your .gitignore files exclude. Repeat --ignore '<regex>' to skip more paths, and --include '<regex>' to force-sync paths that a built-in rule or .gitignore would exclude. --env KEY=VALUE passes an environment variable to the build, and --basis-cache-dir sets where the client keeps its sync cache.

To create the sandbox yourself, for example with labels or a jurisdiction, create it first; the build then uses it:

lim gradle create --jurisdiction eu --label env=dev --display-name ci-builder
lim gradle build . --task :app:assembleRelease
import Limrun from '@limrun/api';

const lim = new Limrun({ apiKey: process.env['LIM_API_KEY'] });

const instance = await lim.gradleInstances.create({
  wait: true,
  reuseIfExists: true,
  metadata: { labels: { session: 'ci-build' } },
});

const gradle = await lim.gradleInstances.createClient({ instance });
await gradle.sync('.');

const build = gradle.gradlebuild({ tasks: [':app:assembleRelease'] });
build.stdout.on('data', (line) => console.log(line));
const { exitCode } = await build;

Without --inactivity-timeout, a Gradle sandbox is deleted after 10 minutes of inactivity. In TypeScript, each step is explicit: create the instance, open a client, sync, then build.

For long builds, return as soon as the build is accepted with --detach, read the log later with lim gradle logs, or receive the result on a webhook. Build logs and webhooks covers all three, including the webhook payload's applicationId, versionName, and versionCode.

React Native and Expo projects

Expo managed-workflow projects have no android/ directory. The sandbox detects them, installs dependencies, and runs expo prebuild before Gradle. Two flags tune that pipeline. Setting either one forces the Expo pipeline on, and the build fails when no Expo app is detected:

lim gradle build ./my-monorepo --expo-app-dir apps/mobile

Select tools

The sandbox image includes these mise-managed tools:

ToolIncluded compatibility linesDefault
Node.js (with npm and npx)22, 2422
pnpm9, 10, 1110
Yarn1, 41
Bun1 (stable)1
JavaTemurin 17Temurin 17
bundletool11

Builds use your project's Gradle wrapper. Android SDK, NDK, and CMake packages stay managed by sdkmanager.

Select tool versions, install missing ones, and inspect the result:

lim gradle use node@24 java@temurin-17 pnpm@10
lim gradle tools install
lim gradle tools

What each command does:

Limrun imports [tools] declarations from synced project mise files. Those declarations override package-manager detection and image defaults. Most numeric project requests keep their major version; Ruby, Python, Go, Flutter, Dart, and pre-1.0 tools keep major.minor. Limrun can update patch and minor releases within those lines on its own, and latest opts out of a fixed line.

Each build or run resolves its mise environment once. The first sandbox operation that reads a project mise file runs mise install before exporting that environment; this happens once per sandbox. Without a project mise file, commands use the image defaults. Later changes need an explicit lim gradle tools install, and so does a failed or cancelled first install, which also stops that operation.

To pin an exact release, run lim gradle run -- mise use --pin [email protected]. Later operations respect that override, and lim gradle use node@24 replaces it with a Node 24 selection.

The selected Node is exposed as NODE_BINARY, and mise sets JAVA_HOME. Sandbox paths such as HOME, PATH, and the Android SDK locations stay managed.

Run commands

lim gradle run syncs the current directory, then runs a one-shot shell command after -- in the remote workspace with the selected tools. Pass environment variables with --env, which lim gradle build accepts too:

lim gradle run -- node --version
lim gradle run --env APP_ENV=staging -- npm run generate
lim gradle build . --env APP_ENV=staging
const gradle = await lim.gradleInstances.createClient({ instance });
await gradle.sync('.');
const install = await gradle.run('mise install', { cwd: 'apps/mobile' });
if (install.exitCode !== 0) throw new Error('Tool installation failed');
const result = await gradle.gradlebuild({ env: ['APP_ENV=staging'] });

In TypeScript, sync the project first, then call run to install tools or run a command. run accepts cwd, env, and timeoutSeconds and returns the same streamed process interface as gradlebuild.

CLI options:

Commands stream their output.

Commands and builds share one slot per sandbox: starting a new command or build cancels the one that is running.

Keep builds warm

Reuse the same sandbox for later builds. It keeps its dependency caches and temporary files between builds and commands, until the instance is deleted. The image also ships warmed Gradle caches and a pnpm 10 store, so only another pnpm major needs an initial registry download.

Tools you install yourself last for the sandbox's lifetime. A new sandbox starts from the image again: it does not inherit the previous sandbox's workspace or your installed tools.

Dependencies reinstall only when the installed tool versions change. A selected tool that failed to install doesn't trigger reinstalls or block unrelated commands after the first attempt.

Upload the build artifact

There are two ways to get the artifact out of the sandbox.

Upload to Asset Storage. The artifact lives in Limrun's managed storage, and later creates can install it by name. See Asset Storage.

lim gradle build . --upload myapp.apk
lim android create --install-asset=myapp.apk
const build = gradle.gradlebuild({ upload: { assetName: 'myapp.apk' } });
const { exitCode, signedDownloadUrl } = await build;

The uploaded asset expires 14 days after the last upload by default. Each build upload pushes the expiry out again, so actively rebuilt assets stay alive. Pass --upload-ttl with a Go duration (ttl in the SDK) to change the window: 720h works, 1d is not valid Go syntax. Assets uploaded with lim asset push are unaffected; they never expire unless given a --ttl.

The build result includes a signed download URL. Anyone with the URL can download the artifact without a Limrun API key. The signature expires after 15 minutes; fetch a fresh one with lim asset list --name <asset-name> --download-url.

Upload to your own bucket. Pass a presigned S3, GCS, or R2 URL and the sandbox PUTs the artifact straight to it, without a round trip through the machine that started the build:

lim gradle build . --signed-upload-url '<presigned-url>'
const build = gradle.gradlebuild({ upload: { signedUploadUrl: '<presigned-url>' } });

CLI only: --signed-upload-url defaults to the LIM_SIGNED_UPLOAD_URL environment variable.

To build on every push from a Linux runner, see GitHub Actions recipes.

Troubleshooting

Symptom or messageCauseFix
The build can't find the Gradle rootAuto-discovery found more than one candidate, or the root is nested.Pass --project-path <dir>, for example --project-path android.
--expo-app-dir or --abi fails with no Expo app detectedEither flag forces the Expo pipeline on.Remove the flag for native projects, or point --expo-app-dir at the Expo app.
A new tool selection has no effectThe first mise install already ran for this sandbox.Run lim gradle tools install.
A running build stops with status CANCELLEDAnother build or lim gradle run started on the same sandbox.Run one operation at a time per sandbox, or use separate sandboxes.
--upload-ttl 1d is rejected1d is not a Go duration.Use hours, for example 24h.
The download URL returns an error after a whileSigned download URLs expire after 15 minutes.Run lim asset list --name <asset-name> --download-url for a fresh one.

Next steps

key-round

Sign and publish

Sign a release AAB with your organization's upload key and publish it to Google Play.

tablet-smartphone

Run an emulator

Install the APK the build uploaded and drive the device.

webhook

Build logs and webhooks

Detach from long builds, read logs later, and receive the result on a webhook.

workflow

GitHub Actions recipes

Build and sign on a plain Linux runner.