Compare commits
3
Commits
a7a33527c4
...
f7dc76c955
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7dc76c955 | ||
|
|
ed975019dc | ||
|
|
0c7f34ab90 |
@@ -96,34 +96,71 @@ command. The emulator is addressed by its saved pid in
|
||||
|
||||
## The current state of the build
|
||||
|
||||
`make android-smoke` **fails today, and the cause is known.**
|
||||
`backend/system`'s `buildUserDirPath` switches on `runtime.GOOS` with
|
||||
cases for darwin, linux and windows, and a `default:` that returns
|
||||
`errUnsupportedOS`. `runtime.GOOS` is `"android"`, so it takes the
|
||||
default, `NewYellowJacketApp` fails, and `main()` calls `os.Exit(1)` —
|
||||
about 6 ms after the bridge initialises, which is exactly the signature
|
||||
described above.
|
||||
**The app starts. The x86_64 emulator cannot run it, and that is not a
|
||||
bug in the app.**
|
||||
|
||||
**The fix is a documented Wails API and needs no build tags.**
|
||||
`application.Mobile.StoragePath()` returns the app's private files
|
||||
directory and returns `""` on desktop (`mobile_stub.go`), and
|
||||
`resolveUserDirPath` already lets `YJ_HOME` override the path on every
|
||||
OS — so setting that override from `StoragePath()` early in `main()`,
|
||||
when it is non-empty, is the whole change. Do *not* import
|
||||
`pkg/application` into `backend/system`: that package is deliberately
|
||||
Wails-free, which is what the `indexbuild` tag split is protecting.
|
||||
`modernc.org/libc` — which `modernc.org/sqlite`, and therefore the whole
|
||||
database layer, sits on — issues a **raw `lstat` syscall on
|
||||
linux/amd64** (`libc_linux_amd64.go`'s `Xlstat64` calls
|
||||
`unix.Syscall(unix.SYS_LSTAT, …)`). Android's seccomp policy forbids
|
||||
syscall 6 on x86_64, because bionic never issues it, so the process
|
||||
takes `SIGSYS` the first time anything touches the database:
|
||||
|
||||
It is the *first* thing that stops it, not the only one. MPRIS is
|
||||
compiled in (`android` implies the `linux` build tag, so
|
||||
`mpris_linux.go` is in the build and will look for a session bus that
|
||||
does not exist), and the desktop shell is still a desktop shell. Fixing
|
||||
one and re-running the smoke is how you find the next.
|
||||
```
|
||||
F/libc: Fatal signal 31 (SIGSYS), code 1 (SYS_SECCOMP), syscall 6
|
||||
F/DEBUG: Cause: seccomp prevented call to disallowed x86_64 system call 6
|
||||
```
|
||||
|
||||
**And one that no amount of porting will fix:** open-*directory*
|
||||
dialogs return an error on Android — the Storage Access Framework gives
|
||||
tree URIs, not filesystem paths — as do save-file dialogs. This app's
|
||||
first run is "choose your music folder" and its library model is
|
||||
filesystem paths, so that is a design question, not a port.
|
||||
**arm64 is unaffected, and structurally so.** There is no `lstat`
|
||||
syscall on arm64 at all, so `ccgo_linux_arm64.go`'s `Xlstat` is
|
||||
`Xfstatat(…, AT_SYMLINK_NOFOLLOW)` → `SYS_newfstatat` (79), which
|
||||
Android permits. `grep -c SYS_LSTAT ccgo_linux_arm64.go` is 0. Go's own
|
||||
`syscall` package already uses `fstatat` on both architectures, which
|
||||
is why this is *only* the modernc path.
|
||||
|
||||
So: **verify on arm64, and on this machine that means a real device.**
|
||||
`make android-smoke` on an x86_64 AVD reports a `SIGSYS` tombstone that
|
||||
says nothing about your change.
|
||||
|
||||
**Do not reach for an arm64 system image — it will not run here, and
|
||||
finding that out costs a 3.8 GB download.** Emulator 37 refuses
|
||||
outright:
|
||||
|
||||
```
|
||||
FATAL | Avd's CPU Architecture 'arm64' is not supported by the QEMU2
|
||||
emulator on x86_64 host. System image must match the host
|
||||
architecture.
|
||||
```
|
||||
|
||||
Google dropped cross-architecture emulation; there is no flag. The
|
||||
options are an arm64 host, a physical device, or `adb connect` to one.
|
||||
|
||||
Two consequences worth holding onto. The x86_64 half of the fat APK is
|
||||
*only* useful for emulators, and cannot work on any Android until
|
||||
modernc fixes this — including x86 Chromebooks. And the tombstone is at
|
||||
least honest: unlike the `os.Exit` that came before it, this one leaves
|
||||
a real crash record with a backtrace.
|
||||
|
||||
### What was fixed to get here
|
||||
|
||||
`backend/system`'s `buildUserDirPath` switched on `runtime.GOOS` with a
|
||||
`default:` returning `errUnsupportedOS`, so Android failed at startup
|
||||
and `main()` called `os.Exit(1)` six milliseconds after the bridge came
|
||||
up. `main()` now calls `system.UseHomeOverride(application.Mobile.
|
||||
StoragePath())` before anything asks for a path — a documented,
|
||||
build-tag-free API that returns `""` on desktop, where the setter is a
|
||||
no-op. `backend/system` gained no import of the Wails application
|
||||
package, which matters for the same reason `backend/events` is split by
|
||||
the `indexbuild` tag.
|
||||
|
||||
### What is still not done
|
||||
|
||||
MPRIS is compiled in (`android` implies the `linux` build tag), the
|
||||
shell is still a desktop shell, and — the largest one — open-*directory*
|
||||
dialogs return an error on Android, because the Storage Access Framework
|
||||
yields tree URIs rather than filesystem paths. This app's first run is
|
||||
"choose your music folder" and its library model is filesystem paths, so
|
||||
that is a design question rather than a port.
|
||||
|
||||
## The scaffold's own tasks
|
||||
|
||||
|
||||
@@ -2587,3 +2587,75 @@ under the `Wails` tag and are inspectable from `chrome://inspect`;
|
||||
production builds compile that out — so a debug APK is the more
|
||||
informative one when something is wrong. And the docs recommend
|
||||
`build-tools;35.0.0`; 34.0.0 is what is installed here and builds fine.
|
||||
|
||||
## The app starts on Android; x86_64 Android cannot run it (2026-08-16)
|
||||
|
||||
Two findings, and the second is the one with consequences.
|
||||
|
||||
**The startup bug is fixed.** `backend/system`'s `buildUserDirPath`
|
||||
switched on `runtime.GOOS` and Android took the `default:` branch, so
|
||||
`main()` called `os.Exit(1)` six milliseconds after the JNI bridge came
|
||||
up. `main()` now calls
|
||||
`system.UseHomeOverride(application.Mobile.StoragePath())` before
|
||||
anything asks for a path. `StoragePath()` is `getFilesDir()` on
|
||||
Android, Application Support on iOS and `""` on desktop — where
|
||||
`UseHomeOverride` is a no-op — so the change needs no build tag and
|
||||
alters nothing off mobile. `backend/system` gained no import of the
|
||||
Wails application package, deliberately: that is the same constraint
|
||||
the `indexbuild` split protects in `backend/events`.
|
||||
|
||||
**And then it takes SIGSYS on the x86_64 emulator.**
|
||||
|
||||
```
|
||||
F/libc: Fatal signal 31 (SIGSYS), code 1 (SYS_SECCOMP), syscall 6
|
||||
F/DEBUG: Cause: seccomp prevented call to disallowed x86_64 system call 6
|
||||
```
|
||||
|
||||
Syscall 6 on x86_64 is `lstat`, and the caller is **not our code and
|
||||
not Go's**. Go's `syscall` package already routes both `Stat` and
|
||||
`Lstat` through `fstatat` on amd64 *and* arm64. The caller is
|
||||
`modernc.org/libc`, which `modernc.org/sqlite` sits on and therefore
|
||||
the entire database layer: `libc_linux_amd64.go`'s `Xlstat64` issues
|
||||
`unix.Syscall(unix.SYS_LSTAT, …)` directly. Android's seccomp filter
|
||||
forbids it because bionic never issues it.
|
||||
|
||||
**arm64 is unaffected, structurally rather than by luck.** arm64 has no
|
||||
`lstat` syscall at all, so `ccgo_linux_arm64.go`'s `Xlstat` is
|
||||
`Xfstatat(…, AT_SYMLINK_NOFOLLOW)` → `SYS_newfstatat` (79), which is
|
||||
permitted. `grep -c SYS_LSTAT ccgo_linux_arm64.go` returns 0 against 1
|
||||
for amd64.
|
||||
|
||||
Three consequences:
|
||||
|
||||
- **The default emulator cannot verify this app.** `make android-smoke`
|
||||
on an x86_64 AVD reports a tombstone that says nothing about your
|
||||
change. Verification needs an `arm64-v8a` image (full software
|
||||
emulation on an x86_64 host, so slow) or a real device.
|
||||
- **The x86_64 half of the fat APK is dead weight on every Android**,
|
||||
not just emulators — an x86 Chromebook would hit exactly this. It is
|
||||
31 MB of a 27 MB compressed artifact. Dropping it is a real option;
|
||||
keeping it costs size and buys an emulator target that does not work.
|
||||
Not decided here.
|
||||
- The failure is at least *legible*. Unlike the `os.Exit` it replaced,
|
||||
SIGSYS leaves a tombstone with a backtrace into `libwails.so`, which
|
||||
is how it was identified in one pass.
|
||||
|
||||
Worth knowing for anything else that reaches for a pure-Go C library:
|
||||
this class of bug is invisible to every build and every desktop test,
|
||||
and appears only under a platform's syscall filter.
|
||||
|
||||
### …and the arm64 emulator is not an option on an x86_64 host
|
||||
|
||||
Emulator 37.1.11 refuses outright, after the 3.8 GB image download:
|
||||
|
||||
```
|
||||
FATAL | Avd's CPU Architecture 'arm64' is not supported by the QEMU2
|
||||
emulator on x86_64 host. System image must match the host
|
||||
architecture.
|
||||
```
|
||||
|
||||
Google dropped cross-architecture emulation and there is no flag for
|
||||
it. So the arm64 claim above rests on reading modernc's two code paths,
|
||||
not on having run it: verifying the shipped ABI needs an arm64 host, a
|
||||
physical device, or `adb connect` to one. The image was deleted again;
|
||||
do not re-download it.
|
||||
|
||||
@@ -29,6 +29,29 @@ const (
|
||||
// without touching the current user's real config.toml or yj.db.
|
||||
const envHomeOverride = "YJ_HOME"
|
||||
|
||||
// UseHomeOverride points every config and data path at base, by setting
|
||||
// the same override a development sandbox uses.
|
||||
//
|
||||
// It exists for mobile, where the switch in buildUserDirPath has no
|
||||
// answer: there is no home directory and no XDG, only a per-app private
|
||||
// directory the OS hands out at runtime. The caller is main(), which is
|
||||
// the only place that can ask the platform for it — deliberately, so
|
||||
// this package stays free of the Wails application package that knows
|
||||
// (see backend/events' indexbuild split for why that matters).
|
||||
//
|
||||
// Two rules. An empty base is a no-op, because that is exactly what
|
||||
// application.Mobile.StoragePath() returns on desktop. And an override
|
||||
// that is already set wins, so YJ_HOME on the command line still
|
||||
// relocates a sandbox on a platform that would otherwise decide for
|
||||
// itself.
|
||||
func UseHomeOverride(base string) {
|
||||
if base == "" || os.Getenv(envHomeOverride) != "" {
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.Setenv(envHomeOverride, base)
|
||||
}
|
||||
|
||||
// getUserDirPath returns and creates the path for a user directory.
|
||||
func getUserDirPath(dt dirType) (string, error) {
|
||||
path, err := resolveUserDirPath(dt)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -50,3 +51,39 @@ func TestResolveUserDirPath_NoOverrideUsesOSPath(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// UseHomeOverride carries two rules that a mobile launch depends on and
|
||||
// that nothing else would notice breaking: an empty base must do
|
||||
// nothing, because that is precisely what StoragePath() returns on
|
||||
// desktop, and an override already set must win, or YJ_HOME would stop
|
||||
// relocating a sandbox on the platform that decides for itself.
|
||||
func TestUseHomeOverride(t *testing.T) {
|
||||
const (
|
||||
storage = "/data/user/0/app.yellowjacket/files"
|
||||
sandbox = "/tmp/sandbox"
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
already string
|
||||
base string
|
||||
want string
|
||||
}{
|
||||
{name: "empty base is a no-op", already: "", base: "", want: ""},
|
||||
{name: "sets the override when unset", already: "", base: storage, want: storage},
|
||||
{name: "an existing override wins", already: sandbox, base: storage, want: sandbox},
|
||||
{name: "empty base keeps an existing override", already: sandbox, base: "", want: sandbox},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(envHomeOverride, tt.already)
|
||||
|
||||
UseHomeOverride(tt.base)
|
||||
|
||||
if got := os.Getenv(envHomeOverride); got != tt.want {
|
||||
t.Errorf("%s = %q, want %q", envHomeOverride, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"yellowjacket/backend/assets"
|
||||
"yellowjacket/backend/config"
|
||||
"yellowjacket/backend/profiling"
|
||||
"yellowjacket/backend/system"
|
||||
"yellowjacket/internal/dev"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,20 @@ var (
|
||||
var frontendDistAssets embed.FS
|
||||
|
||||
func main() {
|
||||
// **Mobile has no home directory, and this must run before anything
|
||||
// asks for a path.** backend/system resolves config and data from
|
||||
// $HOME or the OS equivalent, and on Android there is neither: its
|
||||
// switch on runtime.GOOS took the default branch and returned
|
||||
// errUnsupportedOS, so NewYellowJacketApp failed and main() exited
|
||||
// six milliseconds after the JNI bridge came up -- with no panic and
|
||||
// no tombstone, because os.Exit is not a crash.
|
||||
//
|
||||
// StoragePath() is the platform's own answer (getFilesDir() on
|
||||
// Android, Application Support on iOS) and returns "" on desktop,
|
||||
// where UseHomeOverride is then a no-op -- so this needs no build
|
||||
// tag and changes nothing off mobile.
|
||||
system.UseHomeOverride(application.Mobile.StoragePath())
|
||||
|
||||
// WebKitGTK's DMABuf renderer crashes on NVIDIA GPUs under Wayland.
|
||||
// Only disable it for that specific combo so AMD/Intel and X11 users
|
||||
// keep full hardware-accelerated buffer sharing. Users can also
|
||||
|
||||
@@ -162,7 +162,42 @@ cmd_install() {
|
||||
need_sdk
|
||||
[ -f bin/yellowjacket.apk ] || die "no bin/yellowjacket.apk — run 'make android' first"
|
||||
"$ADB" get-state >/dev/null 2>&1 || die "no device — run 'make android-emulator' first"
|
||||
"$ADB" install -r bin/yellowjacket.apk
|
||||
|
||||
# The two ways this fails are both about identity rather than the
|
||||
# build, and neither error says what to do about it.
|
||||
#
|
||||
# A *downgrade* is the versionCode rule working as designed: a bare
|
||||
# `make android` produces versionCode 1, so it will not install over
|
||||
# anything a versioned build left behind. A *signature* mismatch is
|
||||
# the rule this whole pipeline exists for — a debug-signed local
|
||||
# build cannot replace a release-signed one.
|
||||
#
|
||||
# Both are fixed by uninstalling, and on a throwaway emulator that
|
||||
# costs nothing, so say so rather than making someone read the
|
||||
# constant name.
|
||||
out=$("$ADB" install -r bin/yellowjacket.apk 2>&1) || {
|
||||
printf '%s\n' "$out"
|
||||
case "$out" in
|
||||
*INSTALL_FAILED_VERSION_DOWNGRADE*)
|
||||
echo
|
||||
echo "The installed copy has a higher versionCode than this build."
|
||||
echo "A bare 'make android' builds versionCode 1; a versioned one"
|
||||
echo "builds e.g. 10301. Either uninstall:"
|
||||
echo " $ADB uninstall $PKG"
|
||||
echo "or build with a version:"
|
||||
echo " YJ_VERSION=1.3.1 YJ_VERSION_CODE=10301 make android"
|
||||
;;
|
||||
*INSTALL_FAILED_UPDATE_INCOMPATIBLE* | *signatures do not match*)
|
||||
echo
|
||||
echo "The installed copy was signed with a different key. Android"
|
||||
echo "never allows that as an update — which is exactly why CI"
|
||||
echo "refuses to publish a debug-signed APK. Uninstall:"
|
||||
echo " $ADB uninstall $PKG"
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
cmd_launch() {
|
||||
@@ -190,11 +225,17 @@ cmd_smoke() {
|
||||
|
||||
cmd_launch
|
||||
sleep 3
|
||||
local first
|
||||
first=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}')
|
||||
# **`|| true` is load-bearing.** `pidof` exits 1 when it finds
|
||||
# nothing, and under `set -e` a failing command substitution kills
|
||||
# the script -- silently, before it can print why. That is invisible
|
||||
# for as long as the app crash-*loops*, because there is always some
|
||||
# pid; it appears the moment the app dies for good and ActivityManager
|
||||
# stops respawning it, which is exactly the run you most want output
|
||||
# from.
|
||||
local first second
|
||||
first=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}' || true)
|
||||
sleep "$wait_s"
|
||||
local second
|
||||
second=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}')
|
||||
second=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}' || true)
|
||||
|
||||
if [ -n "$first" ] && [ "$first" = "$second" ]; then
|
||||
echo "PASS: $PKG alive as pid $first after ${wait_s}s"
|
||||
|
||||
Reference in New Issue
Block a user