diff --git a/.envrc b/.envrc index 72baf23..1859508 100644 --- a/.envrc +++ b/.envrc @@ -12,5 +12,7 @@ use flake # This must come after `use flake` so FVM Flutter takes precedence. PATH_add .fvm/flutter_sdk/bin +PATH_add "$HOME/Android/Sdk/platform-tools" + # Load variables from .env dotenv_if_exists diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6dae708..a80bec2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,9 @@ repos: - repo: local hooks: - - id: dart-format - name: dart format + - id: dart-check + name: dart format (autofix) + check-fast (parallel) language: system - entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command fvm dart format .' - pass_filenames: false - always_run: true - - id: task-check - name: task check-fast (analyze + unit + widget) - language: system - entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command task check-fast' + entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command scripts/pre_commit_check.sh' pass_filenames: false always_run: true diff --git a/Taskfile.yml b/Taskfile.yml index e140867..3aac278 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -35,7 +35,7 @@ tasks: preconditions: - sh: command -v clang >/dev/null 2>&1 msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev" - - sh: /usr/bin/pkg-config --exists gtk+-3.0 2>/dev/null + - sh: pkg-config --exists gtk+-3.0 2>/dev/null msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev" install-hooks: diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 078a2d0..470293b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -47,10 +47,4 @@ flutter { source = "../.." } -dependencies { - // integration_test is a dev dependency; the Flutter plugin loader adds it as - // debugImplementation only, but GeneratedPluginRegistrant.java (in src/main) - // references its class in all variants. Make it available for release compilation - // without bundling it in the APK. - releaseCompileOnly(project(":integration_test")) -} +dependencies {} diff --git a/android/app/src/release/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java b/android/app/src/release/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java new file mode 100644 index 0000000..7ececa0 --- /dev/null +++ b/android/app/src/release/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java @@ -0,0 +1,19 @@ +package dev.flutter.plugins.integration_test; + +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; + +/** + * No-op stub used in release builds. + * The real IntegrationTestPlugin lives in the integration_test SDK package + * which is a dev-only dependency. GeneratedPluginRegistrant references it in + * all build variants, so without this stub the release build throws + * NoClassDefFoundError at startup, aborting plugin registration entirely. + */ +public class IntegrationTestPlugin implements FlutterPlugin { + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {} + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} +} diff --git a/done.md b/done.md index 6441892..3c72d1d 100644 --- a/done.md +++ b/done.md @@ -6,6 +6,41 @@ Tasks get moved from next.md to done.md ## Tasks +## task deploy-android works end-to-end + +The original "Emulator did not become ready within 120 s" was already resolved in +commit `d222638` by running `adb start-server` before booting the AVD; without the +adb daemon running first, the emulator can never register as a device. + +Running `task deploy-android` after that surfaced an Android-specific integration-test +failure: `aliceTile` had 0 widgets at `tester.tap()` time even though the immediately +preceding `pumpUntil(aliceTile)` had just found it. On the slow software-rendered +emulator the route-pop animation finalises during `pumpUntil`'s trailing 300 ms settle +and the tile is briefly absent right after. Fixed in +`integration_test/app_e2e_test.dart` by re-confirming `aliceTile` with a second +`pumpUntil` (5 s timeout) before the tap. + +Bundled with a coherent set of pre-existing infrastructure changes that make the full +pipeline (Linux + Android UI tests, MobSF scan, APK upload) work in `nix develop`: + +- `flake.nix`: adds Linux desktop runtime libs (gtk3, mesa, libGL, libsecret, …) plus + `PKG_CONFIG_PATH`, `LD_LIBRARY_PATH`, `LIBGL_ALWAYS_SOFTWARE=1`, and the libglvnd + vendor-dir env vars so `flutter build linux` and `xvfb-run` work without a real GPU. +- `pubspec.yaml`: pins `path_provider_android` to `>=2.2.0 <2.3.0` to dodge the SIGSEGV + in `libdartjni.so` (FindClassUnchecked) on Android startup with 2.3+. +- `lib/main.dart` + `lib/data/db/database.dart`: resolves the DB path during `main()` + after `WidgetsFlutterBinding.ensureInitialized()` so the path_provider plugin channel + is registered before the first DB access. +- `stalwart-dev/integration_ui_test.sh`: passes `-screen 0 1280x720x24 +iglx` to Xvfb + so GTK3/Flutter can create a GLX OpenGL context under the virtual framebuffer. +- `.envrc`: adds `$HOME/Android/Sdk/platform-tools` to PATH so `adb` resolves outside + `nix develop`. +- `Taskfile.yml`: drops the `/usr/bin/pkg-config` hardcode in favour of PATH so the + nix-provided wrapper is found. +- `.pre-commit-config.yaml` + `scripts/pre_commit_check.sh`: consolidates `dart format` + and `task check-fast` into a single script invoked by one hook (one `nix develop` + startup instead of two). + ## Replace custom search TextField with Flutter SearchBar Replaced the hand-rolled `TextField`-in-`AppBar` search UI with Flutter's built-in `SearchBar` diff --git a/flake.nix b/flake.nix index d6125be..d003b26 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,24 @@ flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; + + # All Linux desktop runtime libraries needed by flutter build linux and + # the UI integration tests (xvfb-run). Kept as a list so we can reuse + # it for both buildInputs and LD_LIBRARY_PATH / PKG_CONFIG_PATH. + linuxDesktopLibs = with pkgs; [ + gtk3 + libsecret + fontconfig + libepoxy + mesa + libGL # libglvnd — vendor-neutral GL/EGL/GLX dispatch layer + at-spi2-core + glib + pango + cairo + gdk-pixbuf + harfbuzz + ]; in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ @@ -22,6 +40,13 @@ # Flutter version manager — needed for host builds (task build-linux, task run) fvm + # Linux desktop build + runtime dependencies (flutter build linux / task run) + ] ++ linuxDesktopLibs ++ (with pkgs; [ + pkg-config + clang + cmake + ninja + # Local IMAP/SMTP dev server for integration tests stalwart-mail @@ -37,12 +62,32 @@ jq sqlite python3 # used by stalwart-dev/start to pick random ports - ]; + ]); shellHook = '' # Disable Flutter telemetry inside dev shell export FLUTTER_SUPPRESS_ANALYTICS=true + # Expose dev headers to cmake's FindPkgConfig. + # The nix pkg-config wrapper works in bash but cmake invokes pkg-config + # as a subprocess and needs PKG_CONFIG_PATH set explicitly. + export PKG_CONFIG_PATH="${pkgs.gtk3.dev}/lib/pkgconfig:${pkgs.glib.dev}/lib/pkgconfig:${pkgs.pango.dev}/lib/pkgconfig:${pkgs.cairo.dev}/lib/pkgconfig:${pkgs.gdk-pixbuf.dev}/lib/pkgconfig:${pkgs.at-spi2-core.dev}/lib/pkgconfig:${pkgs.harfbuzz.dev}/lib/pkgconfig:${pkgs.libsecret}/lib/pkgconfig:${pkgs.fontconfig.dev}/lib/pkgconfig:${pkgs.libepoxy}/lib/pkgconfig:$PKG_CONFIG_PATH" + + # Nix ld uses --no-copy-dt-needed-entries (strict mode): transitive shared-lib + # deps are not followed automatically, so link them explicitly. + export LDFLAGS="-L${pkgs.fontconfig.lib}/lib -lfontconfig $LDFLAGS" + + # Make nix-built runtime libs visible to the dynamic linker so the + # Flutter Linux bundle and integration-ui tests can run. + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath linuxDesktopLibs}:$LD_LIBRARY_PATH" + + # Wire the libglvnd dispatch to the nix mesa vendor ICDs so GTK/Flutter + # can create an OpenGL (EGL + GLX) context under Xvfb without a real GPU. + export __EGL_VENDOR_LIBRARY_DIRS="${pkgs.mesa}/share/glvnd/egl_vendor.d" + export __GLX_VENDOR_LIBRARY_DIRS="${pkgs.mesa}/lib" + export LIBGL_ALWAYS_SOFTWARE=1 + export MESA_LOADER_DRIVER_OVERRIDE=softpipe + echo "SharedInbox Flutter dev environment ready." echo " Analyze : task analyze" echo " Unit tests : task test" diff --git a/integration_test/app_e2e_test.dart b/integration_test/app_e2e_test.dart index d4272ed..28dc86b 100644 --- a/integration_test/app_e2e_test.dart +++ b/integration_test/app_e2e_test.dart @@ -220,6 +220,10 @@ void main() { // ── Navigate to mailboxes ────────────────────────────────────────────── _log('navigate to mailboxes'); + // On the slow Android emulator (software rendering), aliceTile can be + // briefly absent right after pumpUntil's trailing 300ms settle while the + // route-pop animation finalises. Re-confirm it's present before tapping. + await pumpUntil(tester, aliceTile, timeout: const Duration(seconds: 5)); await tester.tap(aliceTile); await pumpUntil(tester, find.text('INBOX')); _log('mailboxes settled'); diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index bf4cad4..8af549b 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -251,10 +251,25 @@ class AppDatabase extends _$AppDatabase { ); } +// Resolved once in main() via initDatabasePath() before runApp(). +String? _dbPath; + +/// Call after WidgetsFlutterBinding.ensureInitialized() so that the +/// path_provider plugin channel is registered before the first DB access. +Future initDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + _dbPath = p.join(dir.path, 'sharedinbox.db'); +} + LazyDatabase _openConnection() { return LazyDatabase(() async { - final dir = await getApplicationSupportDirectory(); - final file = File(p.join(dir.path, 'sharedinbox.db')); + final file = File( + _dbPath ?? + p.join( + (await getApplicationSupportDirectory()).path, + 'sharedinbox.db', + ), + ); return NativeDatabase.createInBackground( file, setup: (db) { diff --git a/lib/main.dart b/lib/main.dart index 10f8811..c17dd01 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sharedinbox/data/db/database.dart'; import 'package:sharedinbox/di.dart'; import 'package:sharedinbox/ui/router.dart'; -void main({List overrides = const []}) { +void main({List overrides = const []}) async { + WidgetsFlutterBinding.ensureInitialized(); + await initDatabasePath(); runApp(ProviderScope(overrides: overrides, child: const SharedInboxApp())); } diff --git a/next.md b/next.md index 3f4760a..470f762 100644 --- a/next.md +++ b/next.md @@ -17,17 +17,3 @@ Git repo should not contain unknown files. Then commit. ## Tasks - -fix: - -[6ms] no emulator running — booting AVD sharedinbox_test -Emulator did not become ready within 120 s -task: Failed to run task "deploy-android": task: Failed to run task "integration-android": exit status 1 - -Why does the emulator not start? Where are the logs? - -After that: task deploy-android - -fix, if it fails. - ---- diff --git a/pubspec.yaml b/pubspec.yaml index f6ee480..8eb5407 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,3 +54,9 @@ dev_dependencies: flutter: uses-material-design: true + +dependency_overrides: + # path_provider_android 2.3+ uses package:jni which crashes on startup + # (SIGSEGV in libdartjni.so FindClassUnchecked — JNI env not ready when + # the Dart VM first calls into it). Pin to 2.2.x which uses Pigeon instead. + path_provider_android: ">=2.2.0 <2.3.0" diff --git a/scripts/pre_commit_check.sh b/scripts/pre_commit_check.sh new file mode 100755 index 0000000..bcd7e36 --- /dev/null +++ b/scripts/pre_commit_check.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Single nix develop session: format first (autofix), then check-fast. +# Flutter's startup lock prevents true parallelism between dart format and flutter analyze. +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" || exit 1 + +fvm dart format . +task check-fast diff --git a/stalwart-dev/integration_ui_test.sh b/stalwart-dev/integration_ui_test.sh index 7640d87..a90d545 100755 --- a/stalwart-dev/integration_ui_test.sh +++ b/stalwart-dev/integration_ui_test.sh @@ -94,6 +94,9 @@ ts "flutter test start" # xvfb-run provides a virtual framebuffer so the Flutter Linux runner has a # display without requiring a real desktop session. No D-Bus or keyring daemon # is needed because the integration tests inject an in-memory SecureStorage. -xvfb-run --auto-servernum fvm flutter test integration_test/ -d linux +# +iglx enables indirect GLX on Xvfb so Flutter/GTK3 can create an OpenGL context +# using mesa's software renderer (LIBGL_ALWAYS_SOFTWARE=1 is set in flake.nix). +xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 +iglx" \ + fvm flutter test integration_test/ -d linux ts "flutter test done"