Asuka
Development

Guides

Development

Asuka dev in depth on each platform, devices, asuka vet, tests and common problems.

asuka dev runs an app and reloads it as you save: on the iOS simulator, an Android emulator or phone, your Mac, PC or Linux machine, or your browser. This page covers how it does that on each, what a reload keeps, devices, assets and logs, what it can't reload, asuka vet, the problems you may meet, and Asuka's own tests and benchmarks.

asuka dev

asuka dev [flags] [dir]                     # this computer: macOS, Windows or Linux
asuka dev --platform ios [flags] [dir]       # the iOS simulator
asuka dev --platform android [flags] [dir]   # an Android emulator or phone
asuka dev --platform web [flags] [dir]       # your browser

--platform is one of ios, android, mac, windows, linux and web; asuka build takes it likewise. Without it, the app runs (or builds) for the computer you're on. One platform per run: run two terminals for two.

dir is the app's directory, the current one by default: a main package that calls app.Run. The flags:

  • --platform: as above.
  • --lang: show a language for this run (de, zh-Hans), or a pseudo locale: en-XA (accented and longer) or ar-XB (right to left). --dir rtl lays any app out right to left. See Languages.
  • --once: build the app and run it once, without watching. On the desktop, asuka dev exits when the app does; on a phone, it builds the app with its Go code, installs it and launches it, then exits, without logs; in a browser, it serves the app.
  • -v: print the commands Asuka runs.
  • --no-vet: don't run asuka vet beside the builds.

On a phone:

  • --device: on iOS, a simulator's name or UDID (by default the booted iPhone, else a recent iPhone Pro, which it boots); on Android, an adb serial or an AVD's name (by default the first device adb lists, else the first AVD, which it starts).
  • --embed: build the app with its Go code in it, and watch.

In a browser: --port, --lan (serve to your network, for a phone's browser) and --no-open; see the web.

On the desktop

On macOS, Windows and Linux, the app's Go code and the platform's host are one Go program, built for this computer with CGO_ENABLED=0. asuka dev builds it and runs it; on each save it builds it again, stops the running app, which saves its signals' values as it quits, and runs the new one, which takes them back (see What a reload keeps). A reload takes about as long as go build. A build that fails prints its errors and keeps the running app.

The app reads its assets/ where they are, so changing one needs no copy. Its data (store.Dir(), SQLite databases there) is in a data directory of its own in Asuka's cache (see Starting over), not the installed app's, so asuka dev and a release don't share it. Its output (fmt.Println, log, panics) prints in the terminal.

macOS. asuka dev on a Mac runs the app from an .app like a release's, in Asuka's cache: its Info.plist (its name, bundle ID, URL scheme, purpose strings and languages from asuka.json) and icon, signed ad hoc. So the app is itself to the system, as when it's opened from the Finder: the Dock shows its name and icon, notifications are its own, and its privacy prompts (the camera, location, contacts) ask for it, with its purpose strings, not for the terminal. Packages that need a provisioning profile's entitlements (push, Sign in with Apple, App Attest) work only in a release build signed with one: see Shipping.

Windows. asuka dev runs only on Windows itself; asuka build --platform windows builds from any system. The dev build is a console program, so its output prints in the terminal; a release has no console. To stop the app for a reload, Asuka closes its standard input (Windows can't send a process Ctrl-C), and it saves its state as it quits.

Linux. asuka dev on Linux runs the app on GTK 4 and libadwaita, which it needs installed (see Getting started), under your session's display, Wayland or X11. It gets the name, ID, version and accent of asuka.json, as a release does; Asuka stops it with SIGINT, and it saves its state as it quits. On a Mac or a PC, asuka dev --platform linux builds the app for Linux but can't run it there, unless ASUKA_LINUX_RUN names a command that runs a program on a Linux machine with a display, which sees this computer's files at the same paths: a VM, as ASUKA_LINUX_RUN="orb -m ubuntu" (OrbStack) or ASUKA_LINUX_RUN="limactl shell default" (Lima). Asuka runs the app with it, for that machine's architecture.

On a phone: the player and built apps

iOS needs a Mac; Android, a Mac or Linux. By default, asuka dev runs your Go code on your computer, and its views in the player: an app named Asuka (dev.asuka.player) that is the platform's host with no app in it. The player is built and installed once, shared by every app, and installed again only when it changes (a new version of Asuka). The Go code and the player talk over a socket.

On a save, Asuka rebuilds the Go code for your computer and restarts it, in about a second; the player reconnects and shows the new code. Nothing is installed.

With --embed or --once, or when the app has native code for the platform (an ios/ or android/ directory, see Native code), Asuka builds the app itself instead, its Go code inside as a release build has it, under the app's own name and ID:

  • iOS: the host, with the app's Swift, is compiled once and cached; each save rebuilds the Go code, links the app, signs it for the simulator and reinstalls it with xcrun simctl.
  • Android: Gradle builds the app, with its Kotlin, once for each version of the host, of the Kotlin and of the manifest; each save rebuilds the Go library, pushes it into the app's private files with adb, where the debug app loads it from, and restarts the app.

The player has every permission any Asuka package needs, and its own purpose strings. What asuka.json gives the app itself (its name, its ID, a URL scheme, links, ios.usage) only a built app has: try those with --embed. The accent color applies in the player too.

What a reload keeps

A reload keeps the values of the app's signals, with no code of yours: in the player, on the desktop and in the browser. The app saves them as it stops, to the file the ASUKA_STATE environment variable names (asuka dev sets it to state.json in the app's directory in Asuka's cache), and the next run gives each value back to the signal made at the same place: the same call path (function names, not lines), the same type, and the same position among the signals of that type made there.

What isn't kept:

  • Navigation: a reload starts at the root screen.
  • Signals Asuka makes itself, and values JSON doesn't carry (funcs, unexported fields).
  • Signals whose type changed, and those of a type after one of the same type you added at the same place.

A built phone app (--embed, --once, native code) starts again from nothing on each reload.

Files your Go code writes (asuka/store, asuka/sqlite) are on your computer, in the player as on the desktop: store.Dir() is a data directory of the app's own in Asuka's cache, one for each platform, as two devices would have theirs. Delete it to start the app again with no data. In the browser they're the page's (IndexedDB): see the web.

Reloading

Asuka watches, by polling, the app's directory and its module's:

  • Go files and go.mod (not _test.go);
  • Swift and Kotlin files;
  • the language catalogs;
  • files under assets/;
  • with --platform web, the app's scripts (web/*.js), and in a checkout of Asuka the web host's (platform/web/src): the page reloads with them, without a build.

It skips directories starting with ., and testdata, build and node_modules. A build that fails prints its errors and keeps the running app; the next save tries again.

asuka.json's fields are read as asuka dev starts. Stop it and run it again after changing them; it rebuilds and reinstalls what changed.

Native code reloads only in a built phone app, where Asuka rebuilds and reinstalls it on each save. Gradle runs again when Kotlin changes, which takes longer than a Go change. A desktop app's native code in Go reloads as the rest of it does; a Mac app's macos/*.swift is compiled again as it changes (a second or two), and the app runs again with its signals' values kept.

Devices

Your computer: asuka dev alone, on a Mac, a PC or Linux.

The iOS simulator shares the Mac's network, so the player reaches asuka dev directly.

An Android emulator reaches your computer at 10.0.2.2, and the player connects there.

An Android phone over USB: turn on USB debugging in its developer options, plug it in and accept the prompt, and check that adb devices lists it. asuka dev --platform android takes the first device adb lists, phone or emulator; with several, name it with --device and its serial. The phone reaches your computer through adb reverse, which Asuka sets up for ports 7461 (the app) and 7462 (its assets). In development Asuka builds for 64-bit phones and emulators (arm64-v8a, x86_64).

An iPhone: asuka dev runs on the simulator only. To run on an iPhone, make a build signed for it, asuka build --platform ios --distribution development (see Shipping), and install the .ipa with Xcode's Devices window.

A phone's browser: asuka dev --platform web --lan serves the app to your network, and prints the addresses to open.

Assets

On the desktop, the app reads assets/ in place. In the player, the app's assets/ directory stays on your computer: the Go process serves it over HTTP, and the player loads ui.Image's asset paths, and the fonts in assets/fonts, from there. In the browser, the dev server serves them. A change to an asset reloads the app.

A built phone app has its assets inside it: on iOS, copied into the app on each build; on Android, pushed into the app's private files when they changed.

Logs

  • On the desktop and in the player, the terminal shows the Go process's output: fmt.Println, log, panics.
  • In the browser, the terminal shows the app's output and the page's errors too.
  • A built app on iOS: the app's output, Go's and Swift's print, through the simulator's console.
  • A built app on Android: the app's logcat. Android drops a process's standard output, so the Go code's goes to logcat, tagged asuka.
  • With --once on a phone, nothing: read them with Console.app or adb logcat.

Backgrounding the app

Leaving the app in the player (to take a photo with the camera app, to sign in in the browser) doesn't end the session. While the player is in the background, the Go code waits up to ten minutes for an answer from it, rather than the usual ten seconds, since the system may freeze it. When Android cuts a backgrounded app's network, the session is kept for three minutes; a player that comes back in time takes it up again, with its screens as they were. A player that comes back later starts the app again, at its root screen.

Stopping asuka dev (Ctrl-C) stops the player too, so it doesn't join the next session, which may be another app's. On the desktop it stops the app.

asuka vet

asuka vet [flags] [packages]

asuka vet finds the mistakes that build and run but do the wrong thing. It reports:

  • reads: a signal read while a component builds its views, so what it shows never changes, as ui.StaticText(fmt.Sprint(count.Get())) or if open.Get() { ... } in a component's body. See What updates.
  • mainthread: a signal written on a goroutine instead of in ui.Post. See The main thread.
  • memowrites: a signal written inside a memo, which panics.
$ asuka vet
main.go:14:23: count.Get() is read once, while App builds its views, so the constant made from it never changes: give the prop signal.Func(func() T { ... }) instead, or use count.Peek() to read it once on purpose

It reports only what it's sure of, so a report is worth fixing. Each message names the fix, and how to say you meant it: a read once on purpose is Peek, or signal.Untrack for an accessor, which it never reports.

The packages are ./... by default, as go vet takes them. So are the flags (-tags=..., -json), and one per check: -reads=false turns reads off. asuka vet exits with 1 when it reports anything, for CI. It runs the same checks as go vet -vettool=$(which asuka), where go vet's own checks don't run.

asuka dev runs it in the background when it starts and after each save, and prints what's new as warnings; asuka build runs it before it builds. Neither stops for what it finds. It takes a second or two, beside a build; --no-vet turns it off for either.

Snapshots and scripts

A desktop app takes a few environment variables that make it show a state and capture it, for checking a change by eye or in CI. Run the app with go run . in its directory:

ASUKA_SNAPSHOT=shot.png go run .                  # the window as a PNG, once it has shown, then quit
ASUKA_SNAPSHOT=shot.png ASUKA_DUMP=1 go run .      # and its view tree, in shot.png.txt
ASUKA_SCRIPT="tap:Add;tap:Add" ASUKA_SNAPSHOT=shot.png go run .   # Windows, Linux: steps first
  • ASUKA_SNAPSHOT writes the window as a PNG once it has shown (and the script has run), and quits; ASUKA_SNAPSHOT_STAY=1 keeps the app running.
  • ASUKA_DUMP writes the view tree beside it: each view's kind, frame and what it shows.
  • ASUKA_SCRIPT (Windows and Linux) runs steps, separated by ;, before the snapshot: tap:<text> (the view showing it: a button's title, a text, a tab), type:<text> (into the focused field), back, key:<shortcut> (ctrl+shift+k), menu:<item> (the menu bar's), escape, close (the window in use), wait:<duration> (500ms) and snap:<file>. Each host has a few more: see its automation.go.

Common problems

  • no iOS simulator SDK (install Xcode): install Xcode, open it once, and add an iOS simulator runtime in its Components settings. no iPhone simulator; add one in Xcode and no simulator "..." mean there's no simulator of that name: xcrun simctl list devices lists them.
  • no Android SDK: install Android Studio or set ANDROID_HOME: Asuka looks in ANDROID_HOME, ANDROID_SDK_ROOT, then ~/Library/Android/sdk on a Mac and ~/Android/Sdk on Linux.
  • no Android NDK: install one from Android Studio's SDK Manager (SDK Tools, NDK).
  • no Android device or emulator: make an AVD in Android Studio's Device Manager, or plug in a phone. Asuka waits up to four minutes for an emulator it starts to boot (the emulator didn't boot).
  • Java: Gradle needs a JDK: JAVA_HOME, else Android Studio's own.
  • The first Android build is slow: Gradle downloads itself and Android's libraries, once.
  • A release build on the phone: an app installed by asuka build (another signing key) is uninstalled, with its data, to install the debug one.
  • asuka dev --platform windows runs the app on this PC: asuka dev runs a Windows app on Windows only. Build it elsewhere with asuka build --platform windows, and copy the folder to a PC.
  • ... isn't installed on Linux: the host needs GTK 4.14 and libadwaita 1.5 or later (the Linux host needs GTK 4.14 and libadwaita 1.5 or later), and some packages a library of their own: WebKitGTK 6.0 for ui.WebView, GStreamer for video and audio, libsecret for asuka/secure. The error names it; install it with your distribution's package manager.
  • A Linux app that doesn't open: it needs a display. Over SSH or in CI, run it under Xvfb (xvfb-run -a), with a session bus (dbus-run-session) for what goes through D-Bus.
  • The CLI and the app's Asuka: the CLI carries the hosts' Swift and Kotlin, and the app's go.mod picks Asuka's Go code. Install the CLI at the version the app requires.
  • Starting over: Asuka's builds, caches, kept state and the apps' development data are in Asuka's cache directory: ~/Library/Caches/asuka on a Mac, ~/.cache/asuka on Linux, %LocalAppData%\asuka on Windows. Delete it to build everything again, with no state or data.

Tests

Asuka's own repository tests each host through the platform's real views.

go test ./...                                      # the core, against host/fake
go test ./host/darwin                              # macOS: real AppKit views
go test ./host/windows                             # Windows: real windows
xvfb-run -a dbus-run-session go test ./host/linux  # Linux: real GTK widgets
swift test --package-path platform/ios             # the Swift host's decoder
node --test platform/web                           # the web host's decoder

go test ./host/darwin -run Conformance and go test ./host/windows -run Conformance check what the host draws for a style (edges, corners, borders, shadows, gradients), measured in a capture of the window; the e2e tests below do the same on the simulator, the emulator and headless Chrome.

GUI tests

The examples run on each platform in e2e/, driven through the debug hosts' automation:

go test -tags e2e ./e2e -run IOS       # the booted simulator, else a recent iPhone
go test -tags e2e ./e2e -run Android   # the device asuka dev picks
go test -tags e2e ./e2e -run Web       # headless Chrome (ASUKA_CHROME: its path)
xvfb-run -a dbus-run-session go test -tags e2e ./e2e -run Linux  # GTK 4, on Linux

Each test builds an example with asuka dev --once (a debug build, with its Go code), then taps, types and reads the screen: through a notification on iOS, a broadcast on Android, the dev server on the web (platform/web/src/automation.js), and a file in the directory ASUKA_COMMANDS names on Linux (host/linux/automation.go). The counter, notes, chat, navigation and languages examples have tests, the same on each platform. They wait for the screen to show what they expect, up to a minute, never for a set time.

The iOS tests need Xcode (xcrun); the Android ones need adb on your PATH, and the SDK and NDK that asuka dev --platform android needs; the Linux ones a display (Xvfb's, in CI) and a session bus. Without them, the tests are skipped. macOS and Windows have no e2e tests yet: their host tests drive real windows instead.

Benchmarks

bench/ compares Asuka with SwiftUI and Jetpack Compose: the same app written in each, timed frame by frame in release builds.

go run ./bench/run -ios        # asuka and SwiftUI on the simulator
go run ./bench/run -android    # asuka and Compose on the running emulator or device

-runs sets how many runs of each (5 by default); the medians go to bench/results. bench/README.md has the scenarios, how they're measured, and the latest results.

asuka.json

asuka dev and asuka build read the same asuka.json; see Shipping for its fields.