Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7dc76c955 | ||
|
|
ed975019dc | ||
|
|
0c7f34ab90 |
No files matched your search
@@ -96,34 +96,71 @@ command. The emulator is addressed by its saved pid in
|
|||||||
|
|
||||||
## The current state of the build
|
## The current state of the build
|
||||||
|
|
||||||
`make android-smoke` **fails today, and the cause is known.**
|
**The app starts. The x86_64 emulator cannot run it, and that is not a
|
||||||
`backend/system`'s `buildUserDirPath` switches on `runtime.GOOS` with
|
bug in the app.**
|
||||||
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 fix is a documented Wails API and needs no build tags.**
|
`modernc.org/libc` — which `modernc.org/sqlite`, and therefore the whole
|
||||||
`application.Mobile.StoragePath()` returns the app's private files
|
database layer, sits on — issues a **raw `lstat` syscall on
|
||||||
directory and returns `""` on desktop (`mobile_stub.go`), and
|
linux/amd64** (`libc_linux_amd64.go`'s `Xlstat64` calls
|
||||||
`resolveUserDirPath` already lets `YJ_HOME` override the path on every
|
`unix.Syscall(unix.SYS_LSTAT, …)`). Android's seccomp policy forbids
|
||||||
OS — so setting that override from `StoragePath()` early in `main()`,
|
syscall 6 on x86_64, because bionic never issues it, so the process
|
||||||
when it is non-empty, is the whole change. Do *not* import
|
takes `SIGSYS` the first time anything touches the database:
|
||||||
`pkg/application` into `backend/system`: that package is deliberately
|
|
||||||
Wails-free, which is what the `indexbuild` tag split is protecting.
|
|
||||||
|
|
||||||
It is the *first* thing that stops it, not the only one. MPRIS is
|
```
|
||||||
compiled in (`android` implies the `linux` build tag, so
|
F/libc: Fatal signal 31 (SIGSYS), code 1 (SYS_SECCOMP), syscall 6
|
||||||
`mpris_linux.go` is in the build and will look for a session bus that
|
F/DEBUG: Cause: seccomp prevented call to disallowed x86_64 system call 6
|
||||||
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.
|
|
||||||
|
|
||||||
**And one that no amount of porting will fix:** open-*directory*
|
**arm64 is unaffected, and structurally so.** There is no `lstat`
|
||||||
dialogs return an error on Android — the Storage Access Framework gives
|
syscall on arm64 at all, so `ccgo_linux_arm64.go`'s `Xlstat` is
|
||||||
tree URIs, not filesystem paths — as do save-file dialogs. This app's
|
`Xfstatat(…, AT_SYMLINK_NOFOLLOW)` → `SYS_newfstatat` (79), which
|
||||||
first run is "choose your music folder" and its library model is
|
Android permits. `grep -c SYS_LSTAT ccgo_linux_arm64.go` is 0. Go's own
|
||||||
filesystem paths, so that is a design question, not a port.
|
`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
|
## 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
|
production builds compile that out — so a debug APK is the more
|
||||||
informative one when something is wrong. And the docs recommend
|
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.
|
`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.
|
// without touching the current user's real config.toml or yj.db.
|
||||||
const envHomeOverride = "YJ_HOME"
|
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.
|
// getUserDirPath returns and creates the path for a user directory.
|
||||||
func getUserDirPath(dt dirType) (string, error) {
|
func getUserDirPath(dt dirType) (string, error) {
|
||||||
path, err := resolveUserDirPath(dt)
|
path, err := resolveUserDirPath(dt)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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/assets"
|
||||||
"yellowjacket/backend/config"
|
"yellowjacket/backend/config"
|
||||||
"yellowjacket/backend/profiling"
|
"yellowjacket/backend/profiling"
|
||||||
|
"yellowjacket/backend/system"
|
||||||
"yellowjacket/internal/dev"
|
"yellowjacket/internal/dev"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,20 @@ var (
|
|||||||
var frontendDistAssets embed.FS
|
var frontendDistAssets embed.FS
|
||||||
|
|
||||||
func main() {
|
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.
|
// WebKitGTK's DMABuf renderer crashes on NVIDIA GPUs under Wayland.
|
||||||
// Only disable it for that specific combo so AMD/Intel and X11 users
|
// Only disable it for that specific combo so AMD/Intel and X11 users
|
||||||
// keep full hardware-accelerated buffer sharing. Users can also
|
// keep full hardware-accelerated buffer sharing. Users can also
|
||||||
|
|||||||
@@ -162,7 +162,42 @@ cmd_install() {
|
|||||||
need_sdk
|
need_sdk
|
||||||
[ -f bin/yellowjacket.apk ] || die "no bin/yellowjacket.apk — run 'make android' first"
|
[ -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" 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() {
|
cmd_launch() {
|
||||||
@@ -190,11 +225,17 @@ cmd_smoke() {
|
|||||||
|
|
||||||
cmd_launch
|
cmd_launch
|
||||||
sleep 3
|
sleep 3
|
||||||
local first
|
# **`|| true` is load-bearing.** `pidof` exits 1 when it finds
|
||||||
first=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}')
|
# 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"
|
sleep "$wait_s"
|
||||||
local second
|
second=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}' || true)
|
||||||
second=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}')
|
|
||||||
|
|
||||||
if [ -n "$first" ] && [ "$first" = "$second" ]; then
|
if [ -n "$first" ] && [ "$first" = "$second" ]; then
|
||||||
echo "PASS: $PKG alive as pid $first after ${wait_s}s"
|
echo "PASS: $PKG alive as pid $first after ${wait_s}s"
|
||||||
|
|||||||
Reference in new issue
Block a user