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) orar-XB(right to left).--dir rtllays any app out right to left. See Languages.--once: build the app and run it once, without watching. On the desktop,asuka devexits 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 runasuka vetbeside 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
--onceon a phone, nothing: read them with Console.app oradb 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()))orif 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 purposeIt 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 firstASUKA_SNAPSHOTwrites the window as a PNG once it has shown (and the script has run), and quits;ASUKA_SNAPSHOT_STAY=1keeps the app running.ASUKA_DUMPwrites 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) andsnap:<file>. Each host has a few more: see itsautomation.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 Xcodeandno simulator "..."mean there's no simulator of that name:xcrun simctl list deviceslists them.no Android SDK: install Android Studio or set ANDROID_HOME: Asuka looks inANDROID_HOME,ANDROID_SDK_ROOT, then~/Library/Android/sdkon a Mac and~/Android/Sdkon 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 devruns a Windows app on Windows only. Build it elsewhere withasuka build --platform windows, and copy the folder to a PC.... isn't installedon 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 forui.WebView, GStreamer for video and audio, libsecret forasuka/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.modpicks 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/asukaon a Mac,~/.cache/asukaon Linux,%LocalAppData%\asukaon 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 decodergo 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 LinuxEach 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.