diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index dc845e4e74..a240553a9c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -34,14 +34,16 @@ on: push: branches: [ "main" ] paths: &macos_paths - # The Xcode project, the entitlements, the Podfile + # The Xcode project and the entitlements - 'macos/**' - # cargokit builds and links these into the app; nothing else in CI links - # the Rust library on macOS + # `hook/build.dart` is what compiles the Rust library into the app now, + # and nothing else in CI links it on macOS + - 'hook/**' - 'crates/**' - 'Cargo.toml' - 'Cargo.lock' - # A plugin added or moved changes what pod install resolves and links + # A dependency added or moved changes what the build hooks produce and + # what gets bundled - 'pubspec.yaml' - 'pubspec.lock' # The vendored packages are submodules, so a gitlink move here is a diff --git a/.gitmodules b/.gitmodules index 9701e0bb7e..84a0ba62e6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,3 +30,7 @@ path = third_party/ish-arm64 url = https://github.com/lollipopkit/ShellBox branch = main +[submodule "packages/flutter_pty"] + path = packages/flutter_pty + url = https://github.com/lollipopkit/flutter_pty + branch = main diff --git a/CLAUDE.md b/CLAUDE.md index d5f099be37..cec9b683b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,15 +30,22 @@ A `Makefile` wraps most common tasks — run `make help` for the full list. Pref - `flutter test test/disk_test.dart` - Run specific test file (or `make test-one TEST=test/disk_test.dart`) - `cargo test --workspace` - Run all Rust tests (parser, FFI shell, monitor) - SSH e2e (opt-in): set `SBM_E2E_SSH_HOST=` in the workspace-root `.env`, then `cargo test -p sbm_parser --test ssh_e2e` — uploads the generated script to the remote, runs it, and compares the parsed result against direct command output; silently skipped when unset -- A widget test whose tree writes to a store must open the Hive box **in memory**: `Hive.openBox('setting_test', bytes: Uint8List(0))`. A `testWidgets` body runs in a fake-async zone, and a real file write started there completes on a callback that zone is no longer pumping — so the box's write lock is never released, `box.close()` in `tearDown` blocks forever, and `flutter test` waits on that process. One such test hangs the whole run, with no failure and no output to say which file did it. Widgets that persist on their own (the floating Agent writes its mode on every change, panes write their width on every drag) hit this without the test writing anything itself. +- A widget test whose tree writes to a store opens the database **in memory**: `SqliteDb.openInMemory()` in `setUp`, `SqliteDb.close` in `tearDown`, and the store's `forTest()` constructor (which only differs by using a distinct store name). Widgets that persist on their own — the floating Agent writes its mode on every change, panes write their width on every drag — write without the test asking them to, so this applies to more trees than it looks like. + - This is also why: a `testWidgets` body runs in a fake-async zone, and a real *file* write started there completes on a callback that zone is no longer pumping. Under Hive that left the box's write lock held, `close()` in `tearDown` blocked forever, and `flutter test` waited on the process — one such test hung the whole run with no failure and no output naming the file. An in-memory database has no such lock, but keeping the writes off disk is still the rule. - Size the view, not the surface, when a test depends on a breakpoint: `tester.view.physicalSize` + `devicePixelRatio`. `setSurfaceSize` changes what the tree is laid out in but not what `MediaQuery` reports, so a "phone" test written that way silently exercises the desktop rendering. - `pumpAndSettle` is not usable on a tree containing a text field or another always-scheduling widget: it waits for a frame in which nothing is scheduled, and then gives up after its 10-minute default. Count the frames out with `pump(duration)` instead. `--timeout 30s` keeps any such mistake from costing ten minutes. ### Rust / FFI - `cargo build -p sbm_ffi` - Build the FFI crate; required before running `flutter test test/frb_parser_test.dart` (`test/rust_lib_helper.dart` loads the dylib from `target/`) -- `flutter_rust_bridge_codegen generate` - Regenerate FRB bindings after changing `crates/sbm_ffi/src/api` (config: `flutter_rust_bridge.yaml`; Dart output `lib/src/rust/`, do not edit generated code) -- App builds link Rust via cargokit inside `crates/sbm_ffi/` (the crate and the Flutter FFI plugin glue share one directory), pinned to `flutter_rust_bridge: 2.12.0` in pubspec +- `flutter_rust_bridge_codegen generate` - Regenerate FRB bindings after changing `crates/sbm_ffi/src/api` (config: `flutter_rust_bridge.yaml`; Dart output `lib/src/rust/`, do not edit generated code). Its `dart fix`/`dart format` pass is scoped to that output directory, so it is safe to run. + - **Never run `flutter_rust_bridge_codegen integrate`.** It is greenfield scaffolding: on this repo it reformats the whole project *and every submodule*, and writes a second Rust crate at `rust/` beside the real one. To see what a template looks like, run `create` in a temp directory instead. +- App builds compile Rust through `hook/build.dart` (Dart build hooks, via `flutter_rust_bridge_hooks` → `native_toolchain_rust`). `crates/sbm_ffi` is **not** a Flutter plugin and the app does not depend on it as a package — the hook names the crate path. So it produces no podspec and no `Package.swift`, and one file covers all five platforms. + - `flutter_rust_bridge` is pinned to `2.13.0-beta.6` in both `pubspec.yaml` and `crates/sbm_ffi/Cargo.toml`, and the two must match or `RustLib.init` throws at startup. The native-assets backend needs `>= 2.13.0-beta.2` and 2.13.0 has no stable release yet; this is the project's only prerelease dependency. + - `crates/sbm_ffi/rust-toolchain.toml` pins the channel and lists every shipped target, which `native_toolchain_rust` requires. A target missing from that list is not an error, it is a silent fallback to the host. + - `packages/flutter_pty` is a fork carrying the same change, for the same reason: upstream ships no `Package.swift` and was the last third-party pod. It uses `native_toolchain_c` rather than `native_toolchain_rust`, and is otherwise identical to upstream 0.4.2. + - **CocoaPods is gone from iOS and macOS.** No `Podfile`, no `Pods/`, no `Pods-Runner` include in the xcconfigs, nothing named Pods in either `project.pbxproj`. Do not add a pod back without a reason: the CocoaPods registry is read-only from 2026-12-02. + - `ios/Flutter/Ish.xcconfig` is still included from `Debug.xcconfig` and `Release.xcconfig` and is what decides whether the iOS Linux engine links. To check it end to end, `xcodebuild -project ios/Runner.xcodeproj -target Runner -configuration Debug -showBuildSettings | rg "SBM_ISH|OTHER_LDFLAGS"`. In a **debug** build the app code is in `Runner.debug.dylib`, not `Runner` — the symbol and `otool -L` checks in that file's own comments are written for a release build and read as "not linked" if pointed at the debug stub. ## Architecture @@ -50,7 +57,7 @@ This is a Flutter application for managing Linux servers with the following key - Parsing is pure functions: parsers emit raw counters; diff/windowed computation (speeds etc.) is provided as pure functions, mutable time-series state stays on the caller side. The FFI boundary holds no mutable state. - The command manifest (cmd name → per-platform command, `SrvBoxSep.` segmenting) lives here too; the `commands::EXTENDED` keys (smartctl, AMD GPU) are split out of the fast status function into `SbStatusExt`, which both callers run minutes apart — smartctl at poll frequency keeps a disk from staying spun down - Script generation is shared as well (`script.rs`: build/install/exec commands + output splitting, locked by `tests/script_compat.rs`); the app calls it via FFI and merges the two functions' output, the monitor executes the script locally on its extended cycle -- `crates/sbm_ffi/` - flutter_rust_bridge binding crate + cargokit Flutter plugin glue in one directory (Dart side generated into `lib/src/rust/`) +- `crates/sbm_ffi/` - flutter_rust_bridge binding crate, built by the root `hook/build.dart` (Dart side generated into `lib/src/rust/`) - `crates/sbm_native/` - Native per-platform sampler, **monitor only** — the app always collects over SSH and has no way to run syscalls on a remote host. `sample()` covers cpu/mem/swap/disks/diskio/net/uptime/host/sys via `sysinfo` (BSD/Windows) or direct procfs/sysfs reads feeding `sbm_parser::linux::parse_*` (Linux); amd/sensors/SMART/battery stay on the shared script, which genuinely needs CLI tools - `monitor/` - Server-side monitoring service (Rust + Svelte frontend), has its own `monitor/CLAUDE.md` - Besides status, it serves the endpoints the app uses for a monitor-backed server — `POST /exec`, `/terminal/ws`, `/fs/*` — plus an in-browser terminal for its own panel. All of them are off by default and configured only in `config.toml`; see the "Remote access" section there for the security model @@ -65,10 +72,10 @@ This is a Flutter application for managing Linux servers with the following key - `lib/data/` - Data layer with models, providers, and storage - `model/` - Data models organized by feature (server, container, ssh, etc.) - `provider/` - Riverpod providers for state management - - `store/` - Local storage implementations using Hive + - `store/` - Local storage implementations over `SqliteStore` (fl_lib) - `lib/view/` - UI layer with pages and widgets - `lib/generated/` - Generated localization files -- `lib/hive/` - Hive adapters for local storage +- `lib/hive/` - Hive adapters, kept only so `HiveImport` can read an upgrading install's old boxes. Nothing writes Hive; the whole directory goes when that import does (TODO in the code) - `lib/src/rust/` - Generated FRB bindings (do not edit) - `packages/` - Vendored Dart forks referenced by path from pubspec (dartssh2, xterm, fl_lib, fl_build, etc.), each a submodule. The exception is `packages/webui`, an in-repo Svelte package (`@serverbox/webui`) of shared UI primitives and design tokens, consumed as a `file:` dependency by both `monitor/frontend` and `website/` - `third_party/ish-arm64` - The iOS Linux engine, a submodule of the `lollipopkit/ShellBox` fork. Not in `packages/` because it is C built by meson and consumed by the Xcode project rather than by pubspec. Which revision builds is the gitlink, not a hash in a script: move it with `git submodule update --remote third_party/ish-arm64` and `git add`. `scripts/build-ish-ios.sh` builds it out of tree into `build/ish/build-/`, so a build never leaves the submodule dirty @@ -77,7 +84,7 @@ This is a Flutter application for managing Linux servers with the following key ### Key Technologies - **State Management**: Riverpod with code generation (riverpod_annotation) -- **Local Storage**: Hive for persistent data with generated adapters +- **Local Storage**: one encrypted SQLite file (`store.db`) via `package:sqlite3`, bundled through Dart build hooks with `source: sqlite3mc` - **SSH/SFTP**: Custom dartssh2 fork for server connections - **Terminal**: Custom xterm.dart fork for SSH terminal interface - **Networking**: dio for HTTP requests @@ -151,7 +158,7 @@ ends, or restore a consumer. - Uses Riverpod providers for dependency injection and state management - Uses Freezed for immutable state models - Providers are organized by feature in `lib/data/provider/` -- State is often persisted using Hive stores in `lib/data/store/` +- State is often persisted using the stores in `lib/data/store/` ### Build System @@ -168,8 +175,10 @@ ends, or restore a consumer. - AGAIN, NEVER run code formatting commands. - USE dependency injection via GetIt for services like Stores, Services and etc. - Generate all l10n files using `flutter gen-l10n` command after modifying ARB files. -- USE `hive_ce` not `hive` package for Hive integration. - - Which no need to config `HiveField` and `HiveType` manually. +- Storage is SQLite, not Hive. `Store` in fl_lib is `sealed`, so a new backend has to be added there as another `part of 'iface.dart'`, not in this repo. + - Most stores are rows in one shared `kv(store, key, value, updated_at)` table, with `value` as JSON — so `get` returns what `jsonDecode` produced and the `fromObj` hook rebuilds the model. `connection_stats` and `agent_conversation` own real tables instead, because every read of them is a range over one server. + - Enums are stored **by name**, never by index: an index silently changes meaning when a case is inserted, and these values outlive the build that wrote them. + - A write that should not count as a user edit — a migration flag, a restore, device-local bookkeeping — passes `updateLastUpdateTsOnSet: false`. `Stores.lastModTime` is read off those timestamps and decides which side of a sync wins. - USE widgets and utilities from `fl_lib` package for common functionalities. - Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc. - You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities. diff --git a/Cargo.lock b/Cargo.lock index 3e1aa9a4f1..5608678e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,9 +1347,9 @@ dependencies = [ [[package]] name = "flutter_rust_bridge" -version = "2.12.0" +version = "2.13.0-beta.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0884853aae8a6517b5b58cf36f55da487f2fe110e1686938eb29b6640aae4a5" +checksum = "2fcb811bbf084de059c7ca7e977531a01fcae1a2b9277a771dfbf1d01ab57bd1" dependencies = [ "allo-isolate", "android_logger", @@ -1376,9 +1376,9 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_macros" -version = "2.12.0" +version = "2.13.0-beta.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce32f35f710ced8c5aa557f023f1a624e737b5460cee2b70fcd3a8df09e1b" +checksum = "d562a203133e4e36459b02acdec2747c0c9b95069e5cd90f1d47a29a9c656555" dependencies = [ "hex", "md-5 0.10.6", diff --git a/TODOS.md b/TODOS.md index a9e34a1e76..d51626bf94 100644 --- a/TODOS.md +++ b/TODOS.md @@ -106,13 +106,51 @@ relay 服务,而不是(或者除了)在本地直接对外提供面板访问;面 决定放哪个 crate/仓库、怎么部署、TLS/域名怎么搞,这和现在"每个 agent 一个 单体二进制"的假设不一样,是本仓库目前唯一的例外 -## sbm_ffi:Swift Package Manager 支持 +## 原生构建全部改走 Dart build hooks,CocoaPods 已移除(只验过 macOS / iOS) -Flutter 3.44 起 SPM 是默认路径,`flutter build ios/macos` 会对 `sbm_ffi` 报 -"The following plugins do not support Swift Package Manager"。项目里其他 13 个 -plugin 都已走 SPM(见生成的 `FlutterGeneratedPluginSwiftPackage/Package.swift`), -只剩 `sbm_ffi` 靠 CocoaPods —— `ios/Podfile.lock` 和 `macos/Podfile.lock` 里都只有 -它一个 pod。混合模式在 3.44 下可用,警告不阻塞构建,但 Flutter 声明未来会变成 error。 +**状态(2026-08-19):已合入。** cargokit 五个接入点和整个 `cargokit/` 目录已删, +`crates/sbm_ffi` 不再是 Flutter plugin,改由根目录 `hook/build.dart` 编译。 +FRB 升到 `2.13.0-beta.6`(Rust 与 pubspec 两处)。 + +已验证:macOS 与 iOS 构建通过,产物里有 `sbm_ffi.framework`,两个 +平台都不再有 `Podfile`。`flutter test test/frb_parser_test.dart` +与 `cargo test --workspace` 通过。 + +`flutter_pty` 也一起做了:`packages/flutter_pty` 是 +[TerminalStudio/flutter_pty](https://github.com/TerminalStudio/flutter_pty) 的 +fork,把同样的五个平台构建集成换成一个 `hook/build.dart`(用 `native_toolchain_c`)。 +上游最后一个版本是 0.4.2(2025-01),没有 `Package.swift`,唯一那个 SwiftPM PR +(#21)只做了 macOS 且无人回应。 + +**未验证:Android / Linux / Windows。** 这三个平台原先分别走 cargokit 的 +gradle plugin 和 CMake,现在改走同一个 hook,但本机没跑过。要在 CI 或对应机器上 +各跑一次 `dart run fl_build -p `。 + +**CocoaPods 已彻底移除。** `Podfile`、`Podfile.lock`、`Pods/`、xcconfig 里的 +`Pods-Runner` include、workspace 里的 `Pods.xcodeproj` 引用、两个 pbxproj 里所有 +Pods 条目都没了。原先担心的 "non-standard Podfile" 其实只是 Flutter 标准模板, +没有自定义 pod 也没有额外逻辑。Watch app 和 widget extension 都验过在 iOS 产物里。 + +两条注意事项: +- **不要用 `flutter_rust_bridge_codegen integrate`。** 它是给新项目的脚手架: + 在本仓库上跑会重新格式化 188 个文件、脚手架出一个新的 `rust/` crate、`hook/`、 + `test_driver/`,并且把 7 个 submodule 也一起格式化。`generate` 是安全的 + (它的 `dart fix` 只作用于生成目录,实测不外溢)。 +- **FRB 2.13.0 至今只有 beta。** native-assets 后端要求 `>= 2.13.0-beta.2`, + stable 仍停在 2.12.0。这是本项目目前唯一钉在 prerelease 上的依赖。 + +以下是当初的分析,留作记录。 + +## sbm_ffi:脱离 CocoaPods 的原分析 + +Flutter 3.44 起 SPM 是默认路径。SPM 本身已经生效——13 个 plugin 走的是生成的 +`{ios,macos}/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift` +——剩下的走 CocoaPods 回退。**回退里有两个 pod,不是一个**:`sbm_ffi` 和 +`flutter_pty`(`~/.pub-cache/hosted/pub.dev/flutter_pty-0.4.2` 只有 podspec, +没有 `Package.swift`)。两个 `Podfile.lock` 的 `DEPENDENCIES` 都列着这两个。 + +时限不是 Flutter 定的:CocoaPods trunk 于 **2026-12-02 永久只读**,Flutter 声明 +回退会在那之后移除,具体日期未定。 卡点:`crates/sbm_ffi/{ios,macos}/sbm_ffi.podspec` 靠 CocoaPods 的 `script_phase` 调 `cargokit/build_pod.sh` 编 Rust,再用 `-force_load` 链接 `libsbm_ffi.a`。 @@ -125,17 +163,31 @@ plugin 都已走 SPM(见生成的 `FlutterGeneratedPluginSwiftPackage/Package.sw - cargokit 上游已于 2026-03-26 归档,irondash/cargokit#106 提了这件事,无人跟进 - flutter_rust_bridge 的 SPM PR(fzyzcjy/flutter_rust_bridge#3315)未合并 -两条候选路线(都还没做): -- **预编译 xcframework**:给 `crates/sbm_ffi/{ios,macos}/sbm_ffi/` 写 `Package.swift`, - 用 `.binaryTarget` 指向预先构建好的 `sbm_ffi.xcframework`,cargo 编译移到 - Makefile / fl_build 的 pre-build 步骤,绕开 sandbox。代价:在 Xcode 里直接 Run - 不会重编 Rust,容易用到旧产物;要额外维护打包脚本和 `-force_load` 处理 -- **Dart native assets**:删掉 cargokit,改用 `hook/build.dart` + - `native_toolchain_rust`,五个平台统一,不再需要 podspec 或 Package.swift。代价: - Flutter native assets 仍是实验特性(`--enable-native-assets`),FRB 侧 - `ExternalLibrary` 的加载方式(`frb_generated.dart` 的 `stem`/`ioDirectory`)要改 - -现状决定:先维持混合模式,等 FRB #3315 合并或 native assets 转正再动。 +**结论改为走 Dart build hooks**,不再等 SPM。原先记的"native assets 仍是实验 +特性(`--enable-native-assets`)"已不成立:build hooks 自 **Flutter 3.38 / +Dart 3.10** 起 stable,无需任何 flag,本项目在 Flutter 3.47 / Dart 3.13。走 hooks +意味着 sbm_ffi 既不需要 podspec 也不需要 `Package.swift`,五个平台统一, +CocoaPods 那条时限对它不再适用。 + +做法:`flutter_rust_bridge_codegen integrate --integration-backend native-assets` +生成 `crates/sbm_ffi/hook/build.dart`(经 `flutter_rust_bridge_hooks` 调 +`native_toolchain_rust`),删掉 cargokit 的 5 个接入点(`{ios,macos}` podspec、 +`{linux,windows}/CMakeLists.txt`、`android/build.gradle`)和 `cargokit/` 目录, +`pubspec.yaml` 的五平台 `ffiPlugin: true` 声明随之去掉。另需新增 +`rust-toolchain.toml`(该后端要求钉具体 toolchain + targets,仓库现在没有); +`crate-type = ["cdylib", "staticlib"]` 已经是对的。 + +**未解的两件事:** +- **后端只有 beta。** 要求 `flutter_rust_bridge_codegen >= 2.13.0-beta.2`,而 + 2.13.0 至今无 stable(最新 `2.13.0-beta.6`),stable 仍是项目现钉的 2.12.0 + (`crates/sbm_ffi/Cargo.toml` 与 `pubspec.yaml` 两处)。要么接受 beta,要么等, + 而 12-02 在前面 +- **`flutter_pty` 不在这条路上。** 它是普通 Flutter plugin 不是 FFI plugin, + hooks 解决不了。sbm_ffi 迁完 Podfile 仍删不掉,除非上游支持 SPM 或本地 fork + +被这条替代的旧路线(留作记录):预编译 xcframework + `.binaryTarget`。代价是在 +Xcode 里直接 Run 不会重编 Rust,容易用到旧产物,还要维护打包脚本和 `-force_load`。 +hooks 路线没有这些问题。 ## monitor:cpu_core_metrics / velocity_metrics 是只写表 @@ -279,6 +331,31 @@ agent 的那条完整路径。 ## Hive → SQLite:分阶段做,以及加密怎么落 +**状态(2026-08-19):两个阶段都已合入。** + +已做: +- `SqliteStore` 在 fl_lib(`store/sqlite.dart`),七个 K-V store 换过去,写入 + 一律 JSON,75 处 `.box.` 已收敛。 +- `connection_stats`(`conn_stat` 表)和 `agent_conversation` + (`agent_conversation` + `agent_active` 两表)已关系化,各带索引。 + `conn_stats_index` 那个未加密的盒子随之消失,明文文件在导入时删除。 +- `HiveImport` 负责一次性导入并记 schema v4。 + +未做 / 遗留: +- **`lib/hive/`、`hive_ce*` 依赖、`main.dart` 里的 `Hive.initFlutter()` 都还在。** + 17 个 TypeAdapter 现在只用于一件事:让 `HiveImport` 读得懂旧盒子。没有任何 + 代码再往 Hive 写。这些要等到没有受支持的安装还停在 Hive 上才能删,`HiveImport` + 和 `SpiLegacyAdapter` 同批(代码里已标 TODO)。 +- `~/Library/Application Support/ServerBox/app.db` 那批残留和 + `sandbox_import.dart` 里对它的特判仍未清理(见本节末尾)。 + +**一处没有覆盖到的路径:v2 记录的导入。** `HiveImport` 会把 `LegacySpiV2` +转成 `Spi`(原 `SpiNestSshMigration` 做的事),但没有测试跑过它 —— +`SpiLegacyAdapter.write` 按设计抛异常,所以测试造不出 v2 的盒子,除非再注册 +一个只在测试里用的可写 adapter,而 Hive 按运行时类型解析写入,两个 adapter +认领同一个类型会让行为取决于注册顺序。覆盖面是「装了 v3 版本之前的包、之后 +一直没启动过」的安装。`test/hive_import_test.dart` 覆盖的是 v3 那条路。 + 想换的理由有三条:Hive 把整个盒子读进内存、compact 靠重写整个文件;它只加密 value, key 和盒子结构在文件里是明文;`hive_ce` 是社区接手的 fork。本机实测的盒子大小: `connection_stats_enc.hive` 227KB、`agent_conversation_enc.hive` 128KB,其余(setting / @@ -308,54 +385,102 @@ provider、backup、SFTP 页面、server 编辑页全被卷进去了。 上一版的改动摊到 34 个文件,主要就是这一条带出来的。 - **K-V blob → 关系化 schema**:不必须,而且只有少数 store 值得。 -第一阶段只动引擎,`Store` 接口不变,上层零改动: +第一阶段只动引擎,`Store` 接口不变: -``` -kv(store TEXT, key TEXT, value BLOB, updated_at INTEGER, PRIMARY KEY(store, key)) +```sql +kv(store TEXT, key TEXT, value TEXT /* JSON */, updated_at INTEGER, PRIMARY KEY(store, key)) ``` `HiveStore` 换成 `SqliteStore`,`get`/`set`/`keys`/`clear`/`lastUpdateTs` 一一对应; `box.watch()` 换成自己发的 per-key 通知,`HivePropListenable` 那套 `_BoxListenerManager` -逻辑可以整个搬过去。providers、backup、UI 一行不用改。 +逻辑可以整个搬过去。 + +两条原先没算到的成本: + +- **`SqliteStore` 必须写在 fl_lib 里。** `Store` 是 `sealed class` + (`packages/fl_lib/lib/src/core/store/iface.dart`),sealed 限制子类与基类同一 + library;`hive.dart` / `pref.dart` / `mock.dart` 都是 `part of 'iface.dart'`。 + 所以这是跨仓库改动,不是 app 内部的事。 +- **"上层零改动"只对 `Store` 的调用方成立,对直接摸 `box` 的地方不成立。** + `rg -n '\bbox\.' lib/` = 75 处 / 18 文件。其中 `data/model/app/bak/backup.dart` + 独占 32 处,全是为了绕开 `lastUpdateTs` 而直接 `box.put/keys/delete/putAll` + ——这些改用已有的 `updateLastUpdateTsOnSet: false` 参数即可,不需要 box。 + store 外部另有 5 处:`core/service/watch_sync.dart`(`box.watch()`)、 + `view/page/server/edit/actions.dart`(`box.keys`)、 + `view/page/setting/entries/app.dart`(备份加密,同 backup.dart)、 + `data/store/migrations/m002_nest_ssh.dart`。 + +序列化随之从 TypeAdapter 二进制换成 JSON。覆盖情况已逐个核对:9 个类都有 +`fromJson`(freezed 模型自带 `toJson`),7 个是 enum(按 name 存),setting 里的 +virt keys 本来就存 `List`,`agent_conversation` 本来就存 JSON Map。 + +**但 17 个 adapter 和 `lib/hive/` 没有删** —— 运行时不再用它们,`HiveImport` +读旧盒子还要靠它们解码。删除时机见本节开头的"未做 / 遗留"。 第二阶段只关系化真正需要 SQL 的: -- **`connection_stats`**:现在为了做时间窗口查询,额外开了一个不加密的 `conn_stats_index` - 盒子,外加手写的 `_rebuildIndexCore` / `_compactIfNeeded` / 每服务器 100 条上限。这一整套 - 在 SQL 里是一条 `DELETE WHERE timestamp < ?` 加一个索引。 -- **`agent_conversation`**:同理。 -- `setting` / `server` / `snippet` / `key` / `history` 留在 K-V。它们都在 10KB 以下, - 关系化只换来迁移工作量——上一版的 diff 就是证据。 - -### 加密:两条路 +- **`connection_stats`**:现在为了做时间窗口查询,额外开了一个 `conn_stats_index` + 盒子,外加手写的 `_rebuildIndexCore` / `_updateIndex` / `_pruneExcessRecords` / + `_compactIfNeeded` / 每服务器 100 条上限。这一整套在 SQL 里是一条 + `DELETE WHERE timestamp < ?` 加一个索引。 + **那个索引盒子是未加密的**——`connection_stats.dart` 开它时没传 `encryptionCipher`, + 本机实测 114 KB 明文存着 110 条 `_<毫秒时间戳>`,比它索引的那个加密盒子 + 还大。这是目前唯一一处现存的泄露,关系化之后随表消失(整库加密,索引也在库内)。 +- **`agent_conversation`**:同理。`fetchForServer` 现在是全表扫加内存排序。 +- `setting` / `server` / `snippet` / `key` / `history` / `docker` / `port_forward` + 留在 K-V。它们都在 10KB 以下,关系化只换来迁移工作量——上一版的 diff 就是证据。 + +### 加密:`package:sqlite3` 3.x + build hooks + +**`sqlcipher_flutter_libs` 这条路没了。** 它已废弃(最后版本 `0.7.0+eol`,0.7.0 起 +不再提供任何功能),README 要求改用 `package:sqlite3` 3.x。 + +替代方案比原方案好,原因是 CocoaPods:`package:sqlite3` 3.5.1 通过 **Dart build +hooks** 打包 SQLite,不是 Flutter plugin,既不产生 podspec 也不需要 `Package.swift`。 +配置就是 pubspec 里一段: + +```yaml +hooks: + user_defines: + sqlite3: + source: sqlite3mc +``` -两条都是 SQLCipher(整库加密,包括 key 和索引)。 +**选 `sqlite3mc` 而不是 `sqlcipher`。** 两者都是整库加密(包括 key 和索引),差别在 +依赖:`sqlcipher` 在 Windows / Linux / Android 链接 OpenSSL,CI 要装系统依赖; +SQLite3MultipleCiphers 的 cipher 实现内置于源码("There is no direct dependency on +external projects"),而且它的 cipher 列表里就有一个 SQLCipher 兼容的 AES-256 方案, +日后要互操作可以切过去。新库没有历史包袱,默认方案即可。 -- **`sqlcipher_flutter_libs` + `package:sqlite3`**:五个平台都支持;Linux 要 `libssl-dev`、 - Windows 要 `choco install openssl`(CI 要改);iOS/macOS 装 SQLCipher pod,README 明确写 - 「依赖任何链接普通 sqlite3 的包都会出事」——目前 `pubspec.lock` 里干净,这条不成立,但 - 以后加依赖时要盯着。同步调用是原生的。 -- **Rust `rusqlite` + FRB**:`libsqlite3-sys` 的 `bundled-sqlcipher-vendored-openssl` 把 - SQLCipher 和 OpenSSL 一起编进去,无系统依赖、无 pod,走现有的 cargokit 出五个平台的产物; - 同步调用靠 `#[frb(sync)]`。代价:每次 K-V 操作过一次 FFI;而且 CLAUDE.md 写的 FFI 边界 - 原则是「不持有可变状态」,一个数据库连接正好是可变状态,要么破例要么专门论证。 - 注意 Windows 上必须用 vendored-openssl 那个 feature,`bundled-sqlcipher` 单独用只在 Unix - 上成立。 +Rust `rusqlite` + FRB 那条路不再考虑:它要先等 sbm_ffi 迁完 build hooks 才能脱离 +CocoaPods(见上面那节),而且 CLAUDE.md 写的 FFI 边界原则是「不持有可变状态」, +一个数据库连接正好是可变状态。 -**倾向第一条**,而且不要 drift。不用 drift 的理由就是上面第二个决定:它是异步优先的, -而异步化 `Store` 正是上一版把改动摊开的原因。 +不用 drift,理由是上面第二个决定:drift 是异步优先的,而异步化 `Store` 正是上一版 +把改动摊到 34 个文件的原因。 -### 两个具体的点 +### 三个具体的点 -**密钥要用 raw key,不是 passphrase。** 上一版是 `PRAGMA key = '$escapedKey'`,SQLCipher 会 -把它当口令做 PBKDF2 派生(默认 256000 轮)。而 `SecureStoreProps.hivePwd` 里存的本来就是 +**密钥要用 raw key,不是 passphrase。** 上一版是 `PRAGMA key = '$escapedKey'`,会被 +当口令做 PBKDF2 派生(默认 256000 轮)。而 `SecureStoreProps.hivePwd` 里存的本来就是 `Hive.generateSecureKey()` 产生的 32 字节随机密钥,直接用 `PRAGMA key = "x'<64位hex>'"` 跳过派生,既快也没有降低强度。 **迁移的形状。** 每个盒子一次,`Stores.init` 之前跑,用现有 cipher 打开 `*_enc.hive` -逐 key 写进 `kv` 表;成功后**保留** `.hive` 文件若干版本再删,理由和 `SandboxImport` 一样 -——复制过的原件留着,出问题可回退,并且加 TODO 标记删除时机。`BackupV2` 是 JSON, -和存储引擎无关,不受影响。 +逐 key 转 JSON 写进 `kv` 表;成功后**保留** `.hive` 文件若干版本再删,理由和 +`SandboxImport` 一样——复制过的原件留着,出问题可回退,并且加 TODO 标记删除时机。 +`BackupV2` 是 JSON,和存储引擎无关,不受影响。 + +**迁移有顺序约束。** 新增 `SchemaVersion` v4 + `m003_hive_to_sqlite`。但 v2→v3 的 +`SpiNestSshMigration` 依赖 `SpiLegacyAdapter` 和 Hive 的 typeId 解码(它直接读 +`store.box`),所以 **m003 必须在 Hive 侧走完 v3 之后跑,m003 落地前不能删 Hive +依赖**。实际次序:保留 Hive 只读能力 → m002 → m003 → 之后的版本里删 Hive。 + +### 测试侧 + +19 个测试文件引用 Hive。各 store 的 `forBox(Box testBox)` 测试构造器换成 +`sqlite3.openInMemory()`。这比 CLAUDE.md 里记的 Hive `bytes: Uint8List(0)` 干净, +而且不会踩 fake-async 下写锁不释放、`flutter test` 整体挂死那个坑。 ### 顺带要清的残留 diff --git a/crates/sbm_ffi/Cargo.toml b/crates/sbm_ffi/Cargo.toml index 79a7def4ba..1d1ba14a74 100644 --- a/crates/sbm_ffi/Cargo.toml +++ b/crates/sbm_ffi/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -flutter_rust_bridge = "=2.12.0" +flutter_rust_bridge = "=2.13.0-beta.6" sbm_parser = { path = "../sbm_parser" } serde_json = "1.0" diff --git a/crates/sbm_ffi/android/.gitignore b/crates/sbm_ffi/android/.gitignore deleted file mode 100644 index 161bdcdaf8..0000000000 --- a/crates/sbm_ffi/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/crates/sbm_ffi/android/build.gradle b/crates/sbm_ffi/android/build.gradle deleted file mode 100644 index 64d5526a0a..0000000000 --- a/crates/sbm_ffi/android/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// The Android Gradle Plugin builds the native code with the Android NDK. - -group 'com.flutter_rust_bridge.sbm_ffi' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - // The Android Gradle Plugin knows how to build native code with the NDK. - classpath 'com.android.tools.build:gradle:7.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - if (project.android.hasProperty("namespace")) { - namespace 'com.flutter_rust_bridge.sbm_ffi' - } - - // Bumping the plugin compileSdkVersion requires all clients of this plugin - // to bump the version in their app. - compileSdkVersion 33 - - // Use the NDK version - // declared in /android/app/build.gradle file of the Flutter project. - // Replace it with a version number if this plugin requires a specfic NDK version. - // (e.g. ndkVersion "23.1.7779620") - ndkVersion android.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - defaultConfig { - minSdkVersion 19 - } -} - -apply from: "../cargokit/gradle/plugin.gradle" -cargokit { - manifestDir = ".." - libname = "sbm_ffi" -} diff --git a/crates/sbm_ffi/android/settings.gradle b/crates/sbm_ffi/android/settings.gradle deleted file mode 100644 index 80764c1a09..0000000000 --- a/crates/sbm_ffi/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'sbm_ffi' diff --git a/crates/sbm_ffi/android/src/main/AndroidManifest.xml b/crates/sbm_ffi/android/src/main/AndroidManifest.xml deleted file mode 100644 index 820f2e5f08..0000000000 --- a/crates/sbm_ffi/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/crates/sbm_ffi/cargokit/.gitignore b/crates/sbm_ffi/cargokit/.gitignore deleted file mode 100644 index cf7bb868c0..0000000000 --- a/crates/sbm_ffi/cargokit/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -.dart_tool -*.iml -!pubspec.lock diff --git a/crates/sbm_ffi/cargokit/LICENSE b/crates/sbm_ffi/cargokit/LICENSE deleted file mode 100644 index d33a5fea52..0000000000 --- a/crates/sbm_ffi/cargokit/LICENSE +++ /dev/null @@ -1,42 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -Copyright 2022 Matej Knopp - -================================================================================ - -MIT LICENSE - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -================================================================================ - -APACHE LICENSE, VERSION 2.0 - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - diff --git a/crates/sbm_ffi/cargokit/README b/crates/sbm_ffi/cargokit/README deleted file mode 100644 index 398474dbc8..0000000000 --- a/crates/sbm_ffi/cargokit/README +++ /dev/null @@ -1,11 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -Experimental repository to provide glue for seamlessly integrating cargo build -with flutter plugins and packages. - -See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ -for a tutorial on how to use Cargokit. - -Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. - diff --git a/crates/sbm_ffi/cargokit/build_pod.sh b/crates/sbm_ffi/cargokit/build_pod.sh deleted file mode 100755 index ed0e0d987d..0000000000 --- a/crates/sbm_ffi/cargokit/build_pod.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/sh -set -e - -BASEDIR=$(dirname "$0") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -BASEDIR=$(cd "$BASEDIR" ; pwd -P) - -# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project -NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` - -export PATH=${NEW_PATH%?} # remove trailing : - -env - -# Platform name (macosx, iphoneos, iphonesimulator) -export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME - -# Arctive architectures (arm64, armv7, x86_64), space separated. -export CARGOKIT_DARWIN_ARCHS=$ARCHS - -# Current build configuration (Debug, Release) -export CARGOKIT_CONFIGURATION=$CONFIGURATION - -# Path to directory containing Cargo.toml. -export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 - -# Temporary directory for build artifacts. -export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR - -# Output directory for final artifacts. -export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME - -# Directory to store built tool artifacts. -export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool - -# Directory inside root project. Not necessarily the top level directory of root project. -export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT - -FLUTTER_EXPORT_BUILD_ENVIRONMENT=( - "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS - "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS -) - -for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" -do - if [[ -f "$path" ]]; then - source "$path" - fi -done - -sh "$BASEDIR/run_build_tool.sh" build-pod "$@" - -# Make a symlink from built framework to phony file, which will be used as input to -# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate -# attribute on custom build phase) -ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" -ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/crates/sbm_ffi/cargokit/build_tool/README.md b/crates/sbm_ffi/cargokit/build_tool/README.md deleted file mode 100644 index a878c27964..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/README.md +++ /dev/null @@ -1,5 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -A sample command-line application with an entrypoint in `bin/`, library code -in `lib/`, and example unit test in `test/`. diff --git a/crates/sbm_ffi/cargokit/build_tool/analysis_options.yaml b/crates/sbm_ffi/cargokit/build_tool/analysis_options.yaml deleted file mode 100644 index 0e16a8b092..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/analysis_options.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# This is copied from Cargokit (which is the official way to use it currently) -# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -linter: - rules: - - prefer_relative_imports - - directives_ordering - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/crates/sbm_ffi/cargokit/build_tool/bin/build_tool.dart b/crates/sbm_ffi/cargokit/build_tool/bin/build_tool.dart deleted file mode 100644 index 97535c1ddc..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/bin/build_tool.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'package:build_tool/build_tool.dart' as build_tool; - -void main(List arguments) { - build_tool.runMain(arguments); -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/build_tool.dart b/crates/sbm_ffi/cargokit/build_tool/lib/build_tool.dart deleted file mode 100644 index 191a11c375..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/build_tool.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'src/build_tool.dart' as build_tool; - -Future runMain(List args) async { - return build_tool.runMain(args); -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/android_environment.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/android_environment.dart deleted file mode 100644 index ab7f9c318c..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/android_environment.dart +++ /dev/null @@ -1,196 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; -import 'package:version/version.dart'; - -import 'target.dart'; -import 'util.dart'; - -class AndroidEnvironment { - AndroidEnvironment({ - required this.sdkPath, - required this.ndkVersion, - required this.minSdkVersion, - required this.targetTempDir, - required this.target, - }); - - static void clangLinkerWrapper(List args) { - final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; - if (clang == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); - } - final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; - if (target == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); - } - - runCommand(clang, [ - target, - ...args, - ]); - } - - /// Full path to Android SDK. - final String sdkPath; - - /// Full version of Android NDK. - final String ndkVersion; - - /// Minimum supported SDK version. - final int minSdkVersion; - - /// Target directory for build artifacts. - final String targetTempDir; - - /// Target being built. - final Target target; - - bool ndkIsInstalled() { - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); - return ndkPackageXml.existsSync(); - } - - void installNdk({ - required String javaHome, - }) { - final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; - final sdkManager = path.join( - sdkPath, - 'cmdline-tools', - 'latest', - 'bin', - 'sdkmanager$sdkManagerExtension', - ); - - log.info('Installing NDK $ndkVersion'); - runCommand(sdkManager, [ - '--install', - 'ndk;$ndkVersion', - ], environment: { - 'JAVA_HOME': javaHome, - }); - } - - Future> buildEnvironment() async { - final hostArch = Platform.isMacOS - ? "darwin-x86_64" - : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); - - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final toolchainPath = path.join( - ndkPath, - 'toolchains', - 'llvm', - 'prebuilt', - hostArch, - 'bin', - ); - - final minSdkVersion = - math.max(target.androidMinSdkVersion!, this.minSdkVersion); - - final exe = Platform.isWindows ? '.exe' : ''; - - final arKey = 'AR_${target.rust}'; - final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] - .map((e) => path.join(toolchainPath, e)) - .firstWhereOrNull((element) => File(element).existsSync()); - if (arValue == null) { - throw Exception('Failed to find ar for $target in $toolchainPath'); - } - - final targetArg = '--target=${target.rust}$minSdkVersion'; - - final ccKey = 'CC_${target.rust}'; - final ccValue = path.join(toolchainPath, 'clang$exe'); - final cfFlagsKey = 'CFLAGS_${target.rust}'; - final cFlagsValue = targetArg; - - final cxxKey = 'CXX_${target.rust}'; - final cxxValue = path.join(toolchainPath, 'clang++$exe'); - final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; - final cxxFlagsValue = targetArg; - - final linkerKey = - 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); - - final ranlibKey = 'RANLIB_${target.rust}'; - final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); - - final ndkVersionParsed = Version.parse(ndkVersion); - final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; - final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); - - final runRustTool = - Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; - - final packagePath = (await Isolate.resolvePackageUri( - Uri.parse('package:build_tool/buildtool.dart')))! - .toFilePath(); - final selfPath = path.canonicalize(path.join( - packagePath, - '..', - '..', - '..', - runRustTool, - )); - - // Make sure that run_build_tool is working properly even initially launched directly - // through dart run. - final toolTempDir = - Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; - - return { - arKey: arValue, - ccKey: ccValue, - cfFlagsKey: cFlagsValue, - cxxKey: cxxValue, - cxxFlagsKey: cxxFlagsValue, - ranlibKey: ranlibValue, - rustFlagsKey: rustFlagsValue, - linkerKey: selfPath, - // Recognized by main() so we know when we're acting as a wrapper - '_CARGOKIT_NDK_LINK_TARGET': targetArg, - '_CARGOKIT_NDK_LINK_CLANG': ccValue, - 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, - }; - } - - // Workaround for libgcc missing in NDK23, inspired by cargo-ndk - String _libGccWorkaround(String buildDir, Version ndkVersion) { - final workaroundDir = path.join( - buildDir, - 'cargokit', - 'libgcc_workaround', - '${ndkVersion.major}', - ); - Directory(workaroundDir).createSync(recursive: true); - if (ndkVersion.major >= 23) { - File(path.join(workaroundDir, 'libgcc.a')) - .writeAsStringSync('INPUT(-lunwind)'); - } else { - // Other way around, untested, forward libgcc.a from libunwind once Rust - // gets updated for NDK23+. - File(path.join(workaroundDir, 'libunwind.a')) - .writeAsStringSync('INPUT(-lgcc)'); - } - - var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; - if (rustFlags.isNotEmpty) { - rustFlags = '$rustFlags\x1f'; - } - rustFlags = '$rustFlags-L\x1f$workaroundDir'; - return rustFlags; - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/artifacts_provider.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/artifacts_provider.dart deleted file mode 100644 index c09c362f5a..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/artifacts_provider.dart +++ /dev/null @@ -1,267 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'builder.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'rustup.dart'; -import 'target.dart'; - -class Artifact { - /// File system location of the artifact. - final String path; - - /// Actual file name that the artifact should have in destination folder. - final String finalFileName; - - AritifactType get type { - if (finalFileName.endsWith('.dll') || - finalFileName.endsWith('.dll.lib') || - finalFileName.endsWith('.pdb') || - finalFileName.endsWith('.so') || - finalFileName.endsWith('.dylib')) { - return AritifactType.dylib; - } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { - return AritifactType.staticlib; - } else { - throw Exception('Unknown artifact type for $finalFileName'); - } - } - - Artifact({ - required this.path, - required this.finalFileName, - }); -} - -final _log = Logger('artifacts_provider'); - -class ArtifactProvider { - ArtifactProvider({ - required this.environment, - required this.userOptions, - }); - - final BuildEnvironment environment; - final CargokitUserOptions userOptions; - - Future>> getArtifacts(List targets) async { - final result = await _getPrecompiledArtifacts(targets); - - final pendingTargets = List.of(targets); - pendingTargets.removeWhere((element) => result.containsKey(element)); - - if (pendingTargets.isEmpty) { - return result; - } - - final rustup = Rustup(); - for (final target in targets) { - final builder = RustBuilder(target: target, environment: environment); - builder.prepare(rustup); - _log.info('Building ${environment.crateInfo.packageName} for $target'); - final targetDir = await builder.build(); - // For local build accept both static and dynamic libraries. - final artifactNames = { - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.dylib, - remote: false, - ), - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.staticlib, - remote: false, - ) - }; - final artifacts = artifactNames - .map((artifactName) => Artifact( - path: path.join(targetDir, artifactName), - finalFileName: artifactName, - )) - .where((element) => File(element.path).existsSync()) - .toList(); - result[target] = artifacts; - } - return result; - } - - Future>> _getPrecompiledArtifacts( - List targets) async { - if (userOptions.usePrecompiledBinaries == false) { - _log.info('Precompiled binaries are disabled'); - return {}; - } - if (environment.crateOptions.precompiledBinaries == null) { - _log.fine('Precompiled binaries not enabled for this crate'); - return {}; - } - - final start = Stopwatch()..start(); - final crateHash = CrateHash.compute(environment.manifestDir, - tempStorage: environment.targetTempDir); - _log.fine( - 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); - - final downloadedArtifactsDir = - path.join(environment.targetTempDir, 'precompiled', crateHash); - Directory(downloadedArtifactsDir).createSync(recursive: true); - - final res = >{}; - - for (final target in targets) { - final requiredArtifacts = getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - remote: true, - ); - final artifactsForTarget = []; - - for (final artifact in requiredArtifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final downloadedPath = path.join(downloadedArtifactsDir, fileName); - if (!File(downloadedPath).existsSync()) { - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - await _tryDownloadArtifacts( - crateHash: crateHash, - fileName: fileName, - signatureFileName: signatureFileName, - finalPath: downloadedPath, - ); - } - if (File(downloadedPath).existsSync()) { - artifactsForTarget.add(Artifact( - path: downloadedPath, - finalFileName: artifact, - )); - } else { - break; - } - } - - // Only provide complete set of artifacts. - if (artifactsForTarget.length == requiredArtifacts.length) { - _log.fine('Found precompiled artifacts for $target'); - res[target] = artifactsForTarget; - } - } - - return res; - } - - static Future _get(Uri url, {Map? headers}) async { - int attempt = 0; - const maxAttempts = 10; - while (true) { - try { - return await get(url, headers: headers); - } on SocketException catch (e) { - // Try to detect reset by peer error and retry. - if (attempt++ < maxAttempts && - (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { - _log.severe( - 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); - await Future.delayed(Duration(seconds: 1)); - continue; - } else { - rethrow; - } - } - } - } - - Future _tryDownloadArtifacts({ - required String crateHash, - required String fileName, - required String signatureFileName, - required String finalPath, - }) async { - final precompiledBinaries = environment.crateOptions.precompiledBinaries!; - final prefix = precompiledBinaries.uriPrefix; - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); - _log.fine('Downloading signature from $signatureUrl'); - final signature = await _get(signatureUrl); - if (signature.statusCode == 404) { - _log.warning( - 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); - return; - } - if (signature.statusCode != 200) { - _log.severe( - 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); - return; - } - _log.fine('Downloading binary from $url'); - final res = await _get(url); - if (res.statusCode != 200) { - _log.severe('Failed to download binary $url: status ${res.statusCode}'); - return; - } - if (verify( - precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { - File(finalPath).writeAsBytesSync(res.bodyBytes); - } else { - _log.shout('Signature verification failed! Ignoring binary.'); - } - } -} - -enum AritifactType { - staticlib, - dylib, -} - -AritifactType artifactTypeForTarget(Target target) { - if (target.darwinPlatform != null) { - return AritifactType.staticlib; - } else { - return AritifactType.dylib; - } -} - -List getArtifactNames({ - required Target target, - required String libraryName, - required bool remote, - AritifactType? aritifactType, -}) { - aritifactType ??= artifactTypeForTarget(target); - if (target.darwinArch != null) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.dylib']; - } - } else if (target.rust.contains('-windows-')) { - if (aritifactType == AritifactType.staticlib) { - return ['$libraryName.lib']; - } else { - return [ - '$libraryName.dll', - '$libraryName.dll.lib', - if (!remote) '$libraryName.pdb' - ]; - } - } else if (target.rust.contains('-linux-')) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.so']; - } - } else { - throw Exception("Unsupported target: ${target.rust}"); - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_cmake.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/build_cmake.dart deleted file mode 100644 index 2bc16f93d5..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_cmake.dart +++ /dev/null @@ -1,41 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -class BuildCMake { - final CargokitUserOptions userOptions; - - BuildCMake({required this.userOptions}); - - Future build() async { - final targetPlatform = Environment.targetPlatform; - final target = Target.forFlutterName(Environment.targetPlatform); - if (target == null) { - throw Exception("Unknown target platform: $targetPlatform"); - } - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts([target]); - - final libs = artifacts[target]!; - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path) - .copySync(path.join(Environment.outputDir, lib.finalFileName)); - } - } - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_gradle.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/build_gradle.dart deleted file mode 100644 index 394f60c97d..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_gradle.dart +++ /dev/null @@ -1,50 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -final log = Logger('build_gradle'); - -class BuildGradle { - BuildGradle({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.targetPlatforms.map((arch) { - final target = Target.forFlutterName(arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: true); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - for (final target in targets) { - final libs = artifacts[target]!; - final outputDir = path.join(Environment.outputDir, target.android!); - Directory(outputDir).createSync(recursive: true); - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); - } - } - } - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_pod.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/build_pod.dart deleted file mode 100644 index ce482eca26..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_pod.dart +++ /dev/null @@ -1,90 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; -import 'util.dart'; - -class BuildPod { - BuildPod({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.darwinArchs.map((arch) { - final target = Target.forDarwin( - platformName: Environment.darwinPlatformName, darwinAarch: arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - void performLipo(String targetFile, Iterable sourceFiles) { - runCommand("lipo", [ - '-create', - ...sourceFiles, - '-output', - targetFile, - ]); - } - - final outputDir = Environment.outputDir; - - Directory(outputDir).createSync(recursive: true); - - final staticLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.staticlib) - .toList(); - final dynamicLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.dylib) - .toList(); - - final libName = environment.crateInfo.packageName; - - // If there is static lib, use it and link it with pod - if (staticLibs.isNotEmpty) { - final finalTargetFile = path.join(outputDir, "lib$libName.a"); - performLipo(finalTargetFile, staticLibs.map((e) => e.path)); - } else { - // Otherwise try to replace bundle dylib with our dylib - final bundlePaths = [ - '$libName.framework/Versions/A/$libName', - '$libName.framework/$libName', - ]; - - for (final bundlePath in bundlePaths) { - final targetFile = path.join(outputDir, bundlePath); - if (File(targetFile).existsSync()) { - performLipo(targetFile, dynamicLibs.map((e) => e.path)); - - // Replace absolute id with @rpath one so that it works properly - // when moved to Frameworks. - runCommand("install_name_tool", [ - '-id', - '@rpath/$bundlePath', - targetFile, - ]); - return; - } - } - throw Exception('Unable to find bundle for dynamic library'); - } - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_tool.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/build_tool.dart deleted file mode 100644 index 0cf015dfd9..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/build_tool.dart +++ /dev/null @@ -1,277 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; - -import 'android_environment.dart'; -import 'build_cmake.dart'; -import 'build_gradle.dart'; -import 'build_pod.dart'; -import 'logging.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; -import 'util.dart'; -import 'verify_binaries.dart'; - -final log = Logger('build_tool'); - -abstract class BuildCommand extends Command { - Future runBuildCommand(CargokitUserOptions options); - - @override - Future run() async { - final options = CargokitUserOptions.load(); - - if (options.verboseLogging || - Platform.environment['CARGOKIT_VERBOSE'] == '1') { - enableVerboseLogging(); - } - - await runBuildCommand(options); - } -} - -class BuildPodCommand extends BuildCommand { - @override - final name = 'build-pod'; - - @override - final description = 'Build cocoa pod library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildPod(userOptions: options); - await build.build(); - } -} - -class BuildGradleCommand extends BuildCommand { - @override - final name = 'build-gradle'; - - @override - final description = 'Build android library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildGradle(userOptions: options); - await build.build(); - } -} - -class BuildCMakeCommand extends BuildCommand { - @override - final name = 'build-cmake'; - - @override - final description = 'Build CMake library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildCMake(userOptions: options); - await build.build(); - } -} - -class GenKeyCommand extends Command { - @override - final name = 'gen-key'; - - @override - final description = 'Generate key pair for signing precompiled binaries'; - - @override - void run() { - final kp = generateKey(); - final private = HEX.encode(kp.privateKey.bytes); - final public = HEX.encode(kp.publicKey.bytes); - print("Private Key: $private"); - print("Public Key: $public"); - } -} - -class PrecompileBinariesCommand extends Command { - PrecompileBinariesCommand() { - argParser - ..addOption( - 'repository', - mandatory: true, - help: 'Github repository slug in format owner/name', - ) - ..addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ) - ..addMultiOption('target', - help: 'Rust target triple of artifact to build.\n' - 'Can be specified multiple times or omitted in which case\n' - 'all targets for current platform will be built.') - ..addOption( - 'android-sdk-location', - help: 'Location of Android SDK (if available)', - ) - ..addOption( - 'android-ndk-version', - help: 'Android NDK version (if available)', - ) - ..addOption( - 'android-min-sdk-version', - help: 'Android minimum rquired version (if available)', - ) - ..addOption( - 'temp-dir', - help: 'Directory to store temporary build artifacts', - ) - ..addOption( - 'glibc-version', - help: 'GLIBC version to use for linux builds', - ) - ..addFlag( - "verbose", - abbr: "v", - defaultsTo: false, - help: "Enable verbose logging", - ); - } - - @override - final name = 'precompile-binaries'; - - @override - final description = 'Prebuild and upload binaries\n' - 'Private key must be passed through PRIVATE_KEY environment variable. ' - 'Use gen_key through generate priave key.\n' - 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; - - @override - Future run() async { - final verbose = argResults!['verbose'] as bool; - if (verbose) { - enableVerboseLogging(); - } - - final privateKeyString = Platform.environment['PRIVATE_KEY']; - if (privateKeyString == null) { - throw ArgumentError('Missing PRIVATE_KEY environment variable'); - } - final githubToken = Platform.environment['GITHUB_TOKEN']; - if (githubToken == null) { - throw ArgumentError('Missing GITHUB_TOKEN environment variable'); - } - final privateKey = HEX.decode(privateKeyString); - if (privateKey.length != 64) { - throw ArgumentError('Private key must be 64 bytes long'); - } - final manifestDir = argResults!['manifest-dir'] as String; - if (!Directory(manifestDir).existsSync()) { - throw ArgumentError('Manifest directory does not exist: $manifestDir'); - } - String? androidMinSdkVersionString = - argResults!['android-min-sdk-version'] as String?; - int? androidMinSdkVersion; - if (androidMinSdkVersionString != null) { - androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); - if (androidMinSdkVersion == null) { - throw ArgumentError( - 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); - } - } - final targetStrigns = argResults!['target'] as List; - final targets = targetStrigns.map((target) { - final res = Target.forRustTriple(target); - if (res == null) { - throw ArgumentError('Invalid target: $target'); - } - return res; - }).toList(growable: false); - final precompileBinaries = PrecompileBinaries( - privateKey: PrivateKey(privateKey), - githubToken: githubToken, - manifestDir: manifestDir, - repositorySlug: RepositorySlug.full(argResults!['repository'] as String), - targets: targets, - androidSdkLocation: argResults!['android-sdk-location'] as String?, - androidNdkVersion: argResults!['android-ndk-version'] as String?, - androidMinSdkVersion: androidMinSdkVersion, - tempDir: argResults!['temp-dir'] as String?, - glibcVersion: argResults!['glibc-version'] as String?, - ); - - await precompileBinaries.run(); - } -} - -class VerifyBinariesCommand extends Command { - VerifyBinariesCommand() { - argParser.addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ); - } - - @override - final name = "verify-binaries"; - - @override - final description = 'Verifies published binaries\n' - 'Checks whether there is a binary published for each targets\n' - 'and checks the signature.'; - - @override - Future run() async { - final manifestDir = argResults!['manifest-dir'] as String; - final verifyBinaries = VerifyBinaries( - manifestDir: manifestDir, - ); - await verifyBinaries.run(); - } -} - -Future runMain(List args) async { - try { - // Init logging before options are loaded - initLogging(); - - if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { - return AndroidEnvironment.clangLinkerWrapper(args); - } - - final runner = CommandRunner('build_tool', 'Cargokit built_tool') - ..addCommand(BuildPodCommand()) - ..addCommand(BuildGradleCommand()) - ..addCommand(BuildCMakeCommand()) - ..addCommand(GenKeyCommand()) - ..addCommand(PrecompileBinariesCommand()) - ..addCommand(VerifyBinariesCommand()); - - await runner.run(args); - } on ArgumentError catch (e) { - stderr.writeln(e.toString()); - exit(1); - } catch (e, s) { - log.severe(kDoubleSeparator); - log.severe('Cargokit BuildTool failed with error:'); - log.severe(kSeparator); - log.severe(e); - // This tells user to install Rust, there's no need to pollute the log with - // stack trace. - if (e is! RustupNotFoundException) { - log.severe(kSeparator); - log.severe(s); - log.severe(kSeparator); - log.severe('BuildTool arguments: $args'); - } - log.severe(kDoubleSeparator); - exit(1); - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/builder.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/builder.dart deleted file mode 100644 index 070fc08122..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/builder.dart +++ /dev/null @@ -1,210 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'android_environment.dart'; -import 'cargo.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; -import 'util.dart'; - -final _log = Logger('builder'); - -enum BuildConfiguration { - debug, - release, - profile, -} - -extension on BuildConfiguration { - bool get isDebug => this == BuildConfiguration.debug; - String get rustName => switch (this) { - BuildConfiguration.debug => 'debug', - BuildConfiguration.release => 'release', - BuildConfiguration.profile => 'release', - }; -} - -class BuildException implements Exception { - final String message; - - BuildException(this.message); - - @override - String toString() { - return 'BuildException: $message'; - } -} - -class BuildEnvironment { - final BuildConfiguration configuration; - final CargokitCrateOptions crateOptions; - final String targetTempDir; - final String manifestDir; - final CrateInfo crateInfo; - - final bool isAndroid; - final String? androidSdkPath; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? javaHome; - - final String? glibcVersion; - - BuildEnvironment({ - required this.configuration, - required this.crateOptions, - required this.targetTempDir, - required this.manifestDir, - required this.crateInfo, - required this.isAndroid, - this.androidSdkPath, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.javaHome, - this.glibcVersion, - }); - - static BuildConfiguration parseBuildConfiguration(String value) { - // XCode configuration adds the flavor to configuration name. - final firstSegment = value.split('-').first; - final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( - (e) => e.name == firstSegment, - ); - if (buildConfiguration == null) { - _log.warning('Unknown build configuraiton $value, will assume release'); - return BuildConfiguration.release; - } - return buildConfiguration; - } - - static BuildEnvironment fromEnvironment({ - required bool isAndroid, - }) { - final buildConfiguration = - parseBuildConfiguration(Environment.configuration); - final manifestDir = Environment.manifestDir; - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - final crateInfo = CrateInfo.load(manifestDir); - return BuildEnvironment( - configuration: buildConfiguration, - crateOptions: crateOptions, - targetTempDir: Environment.targetTempDir, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: isAndroid, - androidSdkPath: isAndroid ? Environment.sdkPath : null, - androidNdkVersion: isAndroid ? Environment.ndkVersion : null, - androidMinSdkVersion: - isAndroid ? int.parse(Environment.minSdkVersion) : null, - javaHome: isAndroid ? Environment.javaHome : null, - ); - } -} - -class RustBuilder { - final Target target; - final BuildEnvironment environment; - - RustBuilder({ - required this.target, - required this.environment, - }); - - void prepare( - Rustup rustup, - ) { - final toolchain = _toolchain; - if (rustup.installedTargets(toolchain) == null) { - rustup.installToolchain(toolchain); - } - if (toolchain == 'nightly') { - rustup.installRustSrcForNightly(); - } - if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { - rustup.installTarget(target.rust, toolchain: toolchain); - } - if (environment.glibcVersion != null) { - rustup.installZigBuild(toolchain); - } - } - - CargoBuildOptions? get _buildOptions => - environment.crateOptions.cargo[environment.configuration]; - - String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; - - /// Returns the path of directory containing build artifacts. - Future build() async { - final extraArgs = _buildOptions?.flags ?? []; - final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); - runCommand( - 'rustup', - [ - 'run', - _toolchain, - 'cargo', - (target.android == null && environment.glibcVersion != null) - ? 'zigbuild' - : 'build', - ...extraArgs, - '--manifest-path', - manifestPath, - '-p', - environment.crateInfo.packageName, - if (!environment.configuration.isDebug) '--release', - '--target', - target.rust + - ((target.android == null && environment.glibcVersion != null) - ? '.${environment.glibcVersion!}' - : ""), - '--target-dir', - environment.targetTempDir, - ], - environment: await _buildEnvironment(), - ); - return path.join( - environment.targetTempDir, - target.rust, - environment.configuration.rustName, - ); - } - - Future> _buildEnvironment() async { - if (target.android == null) { - return {}; - } else { - final sdkPath = environment.androidSdkPath; - final ndkVersion = environment.androidNdkVersion; - final minSdkVersion = environment.androidMinSdkVersion; - if (sdkPath == null) { - throw BuildException('androidSdkPath is not set'); - } - if (ndkVersion == null) { - throw BuildException('androidNdkVersion is not set'); - } - if (minSdkVersion == null) { - throw BuildException('androidMinSdkVersion is not set'); - } - final env = AndroidEnvironment( - sdkPath: sdkPath, - ndkVersion: ndkVersion, - minSdkVersion: minSdkVersion, - targetTempDir: environment.targetTempDir, - target: target, - ); - if (!env.ndkIsInstalled() && environment.javaHome != null) { - env.installNdk(javaHome: environment.javaHome!); - } - return env.buildEnvironment(); - } - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/cargo.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/cargo.dart deleted file mode 100644 index c6785fa226..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/cargo.dart +++ /dev/null @@ -1,49 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:toml/toml.dart'; - -class ManifestException { - ManifestException(this.message, {required this.fileName}); - - final String? fileName; - final String message; - - @override - String toString() { - if (fileName != null) { - return 'Failed to parse package manifest at $fileName: $message'; - } else { - return 'Failed to parse package manifest: $message'; - } - } -} - -class CrateInfo { - CrateInfo({required this.packageName}); - - final String packageName; - - static CrateInfo parseManifest(String manifest, {final String? fileName}) { - final toml = TomlDocument.parse(manifest); - final package = toml.toMap()['package']; - if (package == null) { - throw ManifestException('Missing package section', fileName: fileName); - } - final name = package['name']; - if (name == null) { - throw ManifestException('Missing package name', fileName: fileName); - } - return CrateInfo(packageName: name); - } - - static CrateInfo load(String manifestDir) { - final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); - final manifest = manifestFile.readAsStringSync(); - return parseManifest(manifest, fileName: manifestFile.path); - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/crate_hash.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/crate_hash.dart deleted file mode 100644 index 2dc20ee119..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/crate_hash.dart +++ /dev/null @@ -1,125 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; -import 'package:path/path.dart' as path; - -class CrateHash { - /// Computes a hash uniquely identifying crate content. This takes into account - /// content all all .rs files inside the src directory, as well as Cargo.toml, - /// Cargo.lock, build.rs and cargokit.yaml. - /// - /// If [tempStorage] is provided, computed hash is stored in a file in that directory - /// and reused on subsequent calls if the crate content hasn't changed. - static String compute(String manifestDir, {String? tempStorage}) { - return CrateHash._( - manifestDir: manifestDir, - tempStorage: tempStorage, - )._compute(); - } - - CrateHash._({ - required this.manifestDir, - required this.tempStorage, - }); - - String _compute() { - final files = getFiles(); - final tempStorage = this.tempStorage; - if (tempStorage != null) { - final quickHash = _computeQuickHash(files); - final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); - quickHashFolder.createSync(recursive: true); - final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); - if (quickHashFile.existsSync()) { - return quickHashFile.readAsStringSync(); - } - final hash = _computeHash(files); - quickHashFile.writeAsStringSync(hash); - return hash; - } else { - return _computeHash(files); - } - } - - /// Computes a quick hash based on files stat (without reading contents). This - /// is used to cache the real hash, which is slower to compute since it involves - /// reading every single file. - String _computeQuickHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - final data = ByteData(8); - for (final file in files) { - input.add(utf8.encode(file.path)); - final stat = file.statSync(); - data.setUint64(0, stat.size); - input.add(data.buffer.asUint8List()); - data.setUint64(0, stat.modified.millisecondsSinceEpoch); - input.add(data.buffer.asUint8List()); - } - - input.close(); - return base64Url.encode(output.events.single.bytes); - } - - String _computeHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - void addTextFile(File file) { - // text Files are hashed by lines in case we're dealing with github checkout - // that auto-converts line endings. - final splitter = LineSplitter(); - if (file.existsSync()) { - final data = file.readAsStringSync(); - final lines = splitter.convert(data); - for (final line in lines) { - input.add(utf8.encode(line)); - } - } - } - - for (final file in files) { - addTextFile(file); - } - - input.close(); - final res = output.events.single; - - // Truncate to 128bits. - final hash = res.bytes.sublist(0, 16); - return hex.encode(hash); - } - - List getFiles() { - final src = Directory(path.join(manifestDir, 'src')); - final files = src - .listSync(recursive: true, followLinks: false) - .whereType() - .toList(); - files.sortBy((element) => element.path); - void addFile(String relative) { - final file = File(path.join(manifestDir, relative)); - if (file.existsSync()) { - files.add(file); - } - } - - addFile('Cargo.toml'); - addFile('Cargo.lock'); - addFile('build.rs'); - addFile('cargokit.yaml'); - return files; - } - - final String manifestDir; - final String? tempStorage; -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/environment.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/environment.dart deleted file mode 100644 index d99f54a3d1..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/environment.dart +++ /dev/null @@ -1,69 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -extension on String { - String resolveSymlink() => File(this).resolveSymbolicLinksSync(); -} - -class Environment { - /// Current build configuration (debug or release). - static String get configuration => - _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); - - static bool get isDebug => configuration == 'debug'; - static bool get isRelease => configuration == 'release'; - - /// Temporary directory where Rust build artifacts are placed. - static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); - - /// Final output directory where the build artifacts are placed. - static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); - - /// Path to the crate manifest (containing Cargo.toml). - static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); - - /// Directory inside root project. Not necessarily root folder. Symlinks are - /// not resolved on purpose. - static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); - - // Pod - - /// Platform name (macosx, iphoneos, iphonesimulator). - static String get darwinPlatformName => - _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); - - /// List of architectures to build for (arm64, armv7, x86_64). - static List get darwinArchs => - _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); - - // Gradle - static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); - static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); - static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); - static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); - static List get targetPlatforms => - _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); - - // CMAKE - static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); - - static String _getEnv(String key) { - final res = Platform.environment[key]; - if (res == null) { - throw Exception("Missing environment variable $key"); - } - return res; - } - - static String _getEnvPath(String key) { - final res = _getEnv(key); - if (Directory(res).existsSync()) { - return res.resolveSymlink(); - } else { - return res; - } - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/logging.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/logging.dart deleted file mode 100644 index 42b6acc598..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/logging.dart +++ /dev/null @@ -1,53 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:logging/logging.dart'; - -const String kSeparator = "--"; -const String kDoubleSeparator = "=="; - -bool _lastMessageWasSeparator = false; - -void _log(LogRecord rec) { - final prefix = '${rec.level.name}: '; - final out = rec.level == Level.SEVERE ? stderr : stdout; - if (rec.message == kSeparator) { - if (!_lastMessageWasSeparator) { - out.write(prefix); - out.writeln('-' * 80); - _lastMessageWasSeparator = true; - } - return; - } else if (rec.message == kDoubleSeparator) { - out.write(prefix); - out.writeln('=' * 80); - _lastMessageWasSeparator = true; - return; - } - out.write(prefix); - out.writeln(rec.message); - _lastMessageWasSeparator = false; -} - -void initLogging() { - Logger.root.level = Level.INFO; - Logger.root.onRecord.listen((LogRecord rec) { - final lines = rec.message.split('\n'); - for (final line in lines) { - if (line.isNotEmpty || lines.length == 1 || line != lines.last) { - _log(LogRecord( - rec.level, - line, - rec.loggerName, - )); - } - } - }); -} - -void enableVerboseLogging() { - Logger.root.level = Level.ALL; -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/options.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/options.dart deleted file mode 100644 index 856b03f26a..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/options.dart +++ /dev/null @@ -1,310 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:source_span/source_span.dart'; -import 'package:yaml/yaml.dart'; - -import 'builder.dart'; -import 'environment.dart'; -import 'rustup.dart'; - -final _log = Logger('options'); - -/// A class for exceptions that have source span information attached. -class SourceSpanException implements Exception { - // This is a getter so that subclasses can override it. - /// A message describing the exception. - String get message => _message; - final String _message; - - // This is a getter so that subclasses can override it. - /// The span associated with this exception. - /// - /// This may be `null` if the source location can't be determined. - SourceSpan? get span => _span; - final SourceSpan? _span; - - SourceSpanException(this._message, this._span); - - /// Returns a string representation of `this`. - /// - /// [color] may either be a [String], a [bool], or `null`. If it's a string, - /// it indicates an ANSI terminal color escape that should be used to - /// highlight the span's text. If it's `true`, it indicates that the text - /// should be highlighted using the default color. If it's `false` or `null`, - /// it indicates that the text shouldn't be highlighted. - @override - String toString({Object? color}) { - if (span == null) return message; - return 'Error on ${span!.message(message, color: color)}'; - } -} - -enum Toolchain { - stable, - beta, - nightly, -} - -class CargoBuildOptions { - final Toolchain toolchain; - final List flags; - - CargoBuildOptions({ - required this.toolchain, - required this.flags, - }); - - static Toolchain _toolchainFromNode(YamlNode node) { - if (node case YamlScalar(value: String name)) { - final toolchain = - Toolchain.values.firstWhereOrNull((element) => element.name == name); - if (toolchain != null) { - return toolchain; - } - } - throw SourceSpanException( - 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', - node.span); - } - - static CargoBuildOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - Toolchain toolchain = Toolchain.stable; - List flags = []; - for (final MapEntry(:key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: 'toolchain')) { - toolchain = _toolchainFromNode(value); - } else if (key case YamlScalar(value: 'extra_flags')) { - if (value case YamlList(nodes: List list)) { - if (list.every((element) { - if (element case YamlScalar(value: String _)) { - return true; - } - return false; - })) { - flags = list.map((e) => e.value as String).toList(); - continue; - } - } - throw SourceSpanException( - 'Extra flags must be a list of strings', value.span); - } else { - throw SourceSpanException( - 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', - key.span); - } - } - return CargoBuildOptions(toolchain: toolchain, flags: flags); - } -} - -extension on YamlMap { - /// Map that extracts keys so that we can do map case check on them. - Map get valueMap => - nodes.map((key, value) => MapEntry(key.value, value)); -} - -class PrecompiledBinaries { - final String uriPrefix; - final PublicKey publicKey; - - PrecompiledBinaries({ - required this.uriPrefix, - required this.publicKey, - }); - - static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { - final bytes = HEX.decode(key); - if (bytes.length != 32) { - throw SourceSpanException( - 'Invalid public key. Must be 32 bytes long.', span); - } - return PublicKey(bytes); - } - - static PrecompiledBinaries parse(YamlNode node) { - if (node case YamlMap(valueMap: Map map)) { - if (map - case { - 'url_prefix': YamlNode urlPrefixNode, - 'public_key': YamlNode publicKeyNode, - }) { - final urlPrefix = switch (urlPrefixNode) { - YamlScalar(value: String urlPrefix) => urlPrefix, - _ => throw SourceSpanException( - 'Invalid URL prefix value.', urlPrefixNode.span), - }; - final publicKey = switch (publicKeyNode) { - YamlScalar(value: String publicKey) => - _publicKeyFromHex(publicKey, publicKeyNode.span), - _ => throw SourceSpanException( - 'Invalid public key value.', publicKeyNode.span), - }; - return PrecompiledBinaries( - uriPrefix: urlPrefix, - publicKey: publicKey, - ); - } - } - throw SourceSpanException( - 'Invalid precompiled binaries value. ' - 'Expected Map with "url_prefix" and "public_key".', - node.span); - } -} - -/// Cargokit options specified for Rust crate. -class CargokitCrateOptions { - CargokitCrateOptions({ - this.cargo = const {}, - this.precompiledBinaries, - }); - - final Map cargo; - final PrecompiledBinaries? precompiledBinaries; - - static CargokitCrateOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - final options = {}; - PrecompiledBinaries? precompiledBinaries; - - for (final entry in node.nodes.entries) { - if (entry - case MapEntry( - key: YamlScalar(value: 'cargo'), - value: YamlNode node, - )) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: String name)) { - final configuration = BuildConfiguration.values - .firstWhereOrNull((element) => element.name == name); - if (configuration != null) { - options[configuration] = CargoBuildOptions.parse(value); - continue; - } - } - throw SourceSpanException( - 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', - key.span); - } - } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { - precompiledBinaries = PrecompiledBinaries.parse(entry.value); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', - entry.key.span); - } - } - return CargokitCrateOptions( - cargo: options, - precompiledBinaries: precompiledBinaries, - ); - } - - static CargokitCrateOptions load({ - required String manifestDir, - }) { - final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); - final file = File.fromUri(uri); - if (file.existsSync()) { - final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); - return parse(contents); - } else { - return CargokitCrateOptions(); - } - } -} - -class CargokitUserOptions { - // When Rustup is installed always build locally unless user opts into - // using precompiled binaries. - static bool defaultUsePrecompiledBinaries() { - return Rustup.executablePath() == null; - } - - CargokitUserOptions({ - required this.usePrecompiledBinaries, - required this.verboseLogging, - }); - - CargokitUserOptions._() - : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), - verboseLogging = false; - - static CargokitUserOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); - bool verboseLogging = false; - - for (final entry in node.nodes.entries) { - if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { - if (entry.value case YamlScalar(value: bool value)) { - usePrecompiledBinaries = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "use_precompiled_binaries". Must be a boolean.', - entry.value.span); - } else if (entry.key case YamlScalar(value: 'verbose_logging')) { - if (entry.value case YamlScalar(value: bool value)) { - verboseLogging = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "verbose_logging". Must be a boolean.', - entry.value.span); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', - entry.key.span); - } - } - return CargokitUserOptions( - usePrecompiledBinaries: usePrecompiledBinaries, - verboseLogging: verboseLogging, - ); - } - - static CargokitUserOptions load() { - String fileName = "cargokit_options.yaml"; - var userProjectDir = Directory(Environment.rootProjectDir); - - while (userProjectDir.parent.path != userProjectDir.path) { - final configFile = File(path.join(userProjectDir.path, fileName)); - if (configFile.existsSync()) { - final contents = loadYamlNode( - configFile.readAsStringSync(), - sourceUrl: configFile.uri, - ); - final res = parse(contents); - if (res.verboseLogging) { - _log.info('Found user options file at ${configFile.path}'); - } - return res; - } - userProjectDir = userProjectDir.parent; - } - return CargokitUserOptions._(); - } - - final bool usePrecompiledBinaries; - final bool verboseLogging; -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/precompile_binaries.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/precompile_binaries.dart deleted file mode 100644 index f1d0a81f51..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/precompile_binaries.dart +++ /dev/null @@ -1,206 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; - -final _log = Logger('precompile_binaries'); - -class PrecompileBinaries { - PrecompileBinaries({ - required this.privateKey, - required this.githubToken, - required this.repositorySlug, - required this.manifestDir, - required this.targets, - this.androidSdkLocation, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.tempDir, - this.glibcVersion, - }); - - final PrivateKey privateKey; - final String githubToken; - final RepositorySlug repositorySlug; - final String manifestDir; - final List targets; - final String? androidSdkLocation; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? tempDir; - final String? glibcVersion; - - static String fileName(Target target, String name) { - return '${target.rust}_$name'; - } - - static String signatureFileName(Target target, String name) { - return '${target.rust}_$name.sig'; - } - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final targets = List.of(this.targets); - if (targets.isEmpty) { - targets.addAll([ - ...Target.buildableTargets(), - if (androidSdkLocation != null) ...Target.androidTargets(), - ]); - } - - _log.info('Precompiling binaries for $targets'); - - final hash = CrateHash.compute(manifestDir); - _log.info('Computed crate hash: $hash'); - - final String tagName = 'precompiled_$hash'; - - final github = GitHub(auth: Authentication.withToken(githubToken)); - final repo = github.repositories; - final release = await _getOrCreateRelease( - repo: repo, - tagName: tagName, - packageName: crateInfo.packageName, - hash: hash, - ); - - final tempDir = this.tempDir != null - ? Directory(this.tempDir!) - : Directory.systemTemp.createTempSync('precompiled_'); - - tempDir.createSync(recursive: true); - - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - - final buildEnvironment = BuildEnvironment( - configuration: BuildConfiguration.release, - crateOptions: crateOptions, - targetTempDir: tempDir.path, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: androidSdkLocation != null, - androidSdkPath: androidSdkLocation, - androidNdkVersion: androidNdkVersion, - androidMinSdkVersion: androidMinSdkVersion, - glibcVersion: glibcVersion, - ); - - final rustup = Rustup(); - - for (final target in targets) { - final artifactNames = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - if (artifactNames.every((name) { - final fileName = PrecompileBinaries.fileName(target, name); - return (release.assets ?? []).any((e) => e.name == fileName); - })) { - _log.info("All artifacts for $target already exist - skipping"); - continue; - } - - _log.info('Building for $target'); - - final builder = - RustBuilder(target: target, environment: buildEnvironment); - builder.prepare(rustup); - final res = await builder.build(); - - final assets = []; - for (final name in artifactNames) { - final file = File(path.join(res, name)); - if (!file.existsSync()) { - throw Exception('Missing artifact: ${file.path}'); - } - - final data = file.readAsBytesSync(); - final create = CreateReleaseAsset( - name: PrecompileBinaries.fileName(target, name), - contentType: "application/octet-stream", - assetData: data, - ); - final signature = sign(privateKey, data); - final signatureCreate = CreateReleaseAsset( - name: signatureFileName(target, name), - contentType: "application/octet-stream", - assetData: signature, - ); - bool verified = verify(public(privateKey), data, signature); - if (!verified) { - throw Exception('Signature verification failed'); - } - assets.add(create); - assets.add(signatureCreate); - } - _log.info('Uploading assets: ${assets.map((e) => e.name)}'); - for (final asset in assets) { - // This seems to be failing on CI so do it one by one - int retryCount = 0; - while (true) { - try { - await repo.uploadReleaseAssets(release, [asset]); - break; - } on Exception catch (e) { - if (retryCount == 10) { - rethrow; - } - ++retryCount; - _log.shout( - 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); - await Future.delayed(Duration(seconds: 2)); - } - } - } - } - - _log.info('Cleaning up'); - tempDir.deleteSync(recursive: true); - } - - Future _getOrCreateRelease({ - required RepositoriesService repo, - required String tagName, - required String packageName, - required String hash, - }) async { - Release release; - try { - _log.info('Fetching release $tagName'); - release = await repo.getReleaseByTagName(repositorySlug, tagName); - } on ReleaseNotFound { - _log.info('Release not found - creating release $tagName'); - release = await repo.createRelease( - repositorySlug, - CreateRelease.from( - tagName: tagName, - name: 'Precompiled binaries ${hash.substring(0, 8)}', - targetCommitish: null, - isDraft: false, - isPrerelease: false, - body: 'Precompiled binaries for crate $packageName, ' - 'crate hash $hash.', - )); - } - return release; - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/rustup.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/rustup.dart deleted file mode 100644 index a5aa5333f9..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/rustup.dart +++ /dev/null @@ -1,150 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; - -import 'util.dart'; - -class _Toolchain { - _Toolchain( - this.name, - this.targets, - ); - - final String name; - final List targets; -} - -class Rustup { - List? installedTargets(String toolchain) { - final targets = _installedTargets(toolchain); - return targets != null ? List.unmodifiable(targets) : null; - } - - void installToolchain(String toolchain) { - log.info("Installing Rust toolchain: $toolchain"); - runCommand("rustup", ['toolchain', 'install', toolchain]); - _installedToolchains - .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); - } - - void installTarget( - String target, { - required String toolchain, - }) { - log.info("Installing Rust target: $target"); - runCommand("rustup", ['target', 'add', '--toolchain', toolchain, target]); - _installedTargets(toolchain)?.add(target); - } - - bool _didInstallZigBuild = false; - - void installZigBuild(String toolchain) { - if (_didInstallZigBuild) { - return; - } - - log.info("Installing Zig build"); - runCommand("rustup", [ - 'run', - toolchain, - 'cargo', - 'install', - '--locked', - 'cargo-zigbuild', - ]); - _didInstallZigBuild = true; - } - - final List<_Toolchain> _installedToolchains; - - Rustup() : _installedToolchains = _getInstalledToolchains(); - - List? _installedTargets(String toolchain) => _installedToolchains - .firstWhereOrNull( - (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) - ?.targets; - - static List<_Toolchain> _getInstalledToolchains() { - String extractToolchainName(String line) { - // ignore (default) after toolchain name - final parts = line.split(' '); - return parts[0]; - } - - final res = runCommand("rustup", ['toolchain', 'list']); - - // To list all non-custom toolchains, we need to filter out lines that - // don't start with "stable", "beta", or "nightly". - Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) - .map(extractToolchainName) - .toList(growable: true); - - return lines - .map( - (name) => _Toolchain( - name, - _getInstalledTargets(name), - ), - ) - .toList(growable: true); - } - - static List _getInstalledTargets(String toolchain) { - final res = runCommand("rustup", [ - 'target', - 'list', - '--toolchain', - toolchain, - '--installed', - ]); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty) - .toList(growable: true); - return lines; - } - - bool _didInstallRustSrcForNightly = false; - - void installRustSrcForNightly() { - if (_didInstallRustSrcForNightly) { - return; - } - // Useful for -Z build-std - runCommand( - "rustup", - ['component', 'add', 'rust-src', '--toolchain', 'nightly'], - ); - _didInstallRustSrcForNightly = true; - } - - static String? executablePath() { - final envPath = Platform.environment['PATH']; - final envPathSeparator = Platform.isWindows ? ';' : ':'; - final home = Platform.isWindows - ? Platform.environment['USERPROFILE'] - : Platform.environment['HOME']; - final paths = [ - if (home != null) path.join(home, '.cargo', 'bin'), - if (envPath != null) ...envPath.split(envPathSeparator), - ]; - for (final p in paths) { - final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; - final rustupPath = path.join(p, rustup); - if (File(rustupPath).existsSync()) { - return rustupPath; - } - } - return null; - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/target.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/target.dart deleted file mode 100644 index b662c42f1c..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/target.dart +++ /dev/null @@ -1,148 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:collection/collection.dart'; - -import 'util.dart'; - -class Target { - Target({ - required this.rust, - this.flutter, - this.android, - this.androidMinSdkVersion, - this.darwinPlatform, - this.darwinArch, - }); - - static final all = [ - Target( - rust: 'armv7-linux-androideabi', - flutter: 'android-arm', - android: 'armeabi-v7a', - androidMinSdkVersion: 16, - ), - Target( - rust: 'aarch64-linux-android', - flutter: 'android-arm64', - android: 'arm64-v8a', - androidMinSdkVersion: 21, - ), - Target( - rust: 'i686-linux-android', - flutter: 'android-x86', - android: 'x86', - androidMinSdkVersion: 16, - ), - Target( - rust: 'x86_64-linux-android', - flutter: 'android-x64', - android: 'x86_64', - androidMinSdkVersion: 21, - ), - Target( - rust: 'x86_64-pc-windows-msvc', - flutter: 'windows-x64', - ), - Target( - rust: 'aarch64-pc-windows-msvc', - flutter: 'windows-arm64', - ), - Target( - rust: 'x86_64-unknown-linux-gnu', - flutter: 'linux-x64', - ), - Target( - rust: 'aarch64-unknown-linux-gnu', - flutter: 'linux-arm64', - ), - Target(rust: 'riscv64gc-unknown-linux-gnu', flutter: 'linux-riscv64'), - Target( - rust: 'x86_64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'x86_64', - ), - Target( - rust: 'aarch64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios', - darwinPlatform: 'iphoneos', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios-sim', - darwinPlatform: 'iphonesimulator', - darwinArch: 'arm64', - ), - Target( - rust: 'x86_64-apple-ios', - darwinPlatform: 'iphonesimulator', - darwinArch: 'x86_64', - ), - ]; - - static Target? forFlutterName(String flutterName) { - return all.firstWhereOrNull((element) => element.flutter == flutterName); - } - - static Target? forDarwin({ - required String platformName, - required String darwinAarch, - }) { - return all.firstWhereOrNull((element) => // - element.darwinPlatform == platformName && - element.darwinArch == darwinAarch); - } - - static Target? forRustTriple(String triple) { - return all.firstWhereOrNull((element) => element.rust == triple); - } - - static List androidTargets() { - return all - .where((element) => element.android != null) - .toList(growable: false); - } - - /// Returns buildable targets on current host platform ignoring Android targets. - static List buildableTargets() { - if (Platform.isLinux) { - // Right now we don't support cross-compiling on Linux. So we just return - // the host target. - final arch = (runCommand('arch', []).stdout as String).trim(); - if (arch == 'aarch64') { - return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; - } else if (arch == 'riscv64') { - return [Target.forRustTriple('riscv64gc-unknown-linux-gnu')!]; - } else { - return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; - } - } - return all.where((target) { - if (Platform.isWindows) { - return target.rust.contains('-windows-'); - } else if (Platform.isMacOS) { - return target.darwinPlatform != null; - } - return false; - }).toList(growable: false); - } - - @override - String toString() { - return rust; - } - - final String? flutter; - final String rust; - final String? android; - final int? androidMinSdkVersion; - final String? darwinPlatform; - final String? darwinArch; -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/util.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/util.dart deleted file mode 100644 index e25cad8d0b..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/util.dart +++ /dev/null @@ -1,173 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'logging.dart'; -import 'rustup.dart'; - -final log = Logger("process"); - -class CommandFailedException implements Exception { - final String executable; - final List arguments; - final ProcessResult result; - - CommandFailedException({ - required this.executable, - required this.arguments, - required this.result, - }); - - @override - String toString() { - final stdout = result.stdout.toString().trim(); - final stderr = result.stderr.toString().trim(); - return [ - "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", - "Returned Exit Code: ${result.exitCode}", - kSeparator, - "STDOUT:", - if (stdout.isNotEmpty) stdout, - kSeparator, - "STDERR:", - if (stderr.isNotEmpty) stderr, - ].join('\n'); - } -} - -class TestRunCommandArgs { - final String executable; - final List arguments; - final String? workingDirectory; - final Map? environment; - final bool includeParentEnvironment; - final bool runInShell; - final Encoding? stdoutEncoding; - final Encoding? stderrEncoding; - - TestRunCommandArgs({ - required this.executable, - required this.arguments, - this.workingDirectory, - this.environment, - this.includeParentEnvironment = true, - this.runInShell = false, - this.stdoutEncoding, - this.stderrEncoding, - }); -} - -class TestRunCommandResult { - TestRunCommandResult({ - this.pid = 1, - this.exitCode = 0, - this.stdout = '', - this.stderr = '', - }); - - final int pid; - final int exitCode; - final String stdout; - final String stderr; -} - -TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; - -ProcessResult runCommand( - String executable, - List arguments, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - Encoding? stdoutEncoding = systemEncoding, - Encoding? stderrEncoding = systemEncoding, -}) { - if (testRunCommandOverride != null) { - final result = testRunCommandOverride!(TestRunCommandArgs( - executable: executable, - arguments: arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding, - )); - return ProcessResult( - result.pid, - result.exitCode, - result.stdout, - result.stderr, - ); - } - log.finer('Running command $executable ${arguments.join(' ')}'); - final res = Process.runSync( - _resolveExecutable(executable), - arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stderrEncoding: stderrEncoding, - stdoutEncoding: stdoutEncoding, - ); - if (res.exitCode != 0) { - throw CommandFailedException( - executable: executable, - arguments: arguments, - result: res, - ); - } else { - return res; - } -} - -class RustupNotFoundException implements Exception { - @override - String toString() { - return [ - ' ', - 'rustup not found in PATH.', - ' ', - 'Maybe you need to install Rust? It only takes a minute:', - ' ', - if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', - if (hasHomebrewRustInPath()) ...[ - '\$ brew unlink rust # Unlink homebrew Rust from PATH', - ], - if (!Platform.isWindows) - "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", - ' ', - ].join('\n'); - } - - static bool hasHomebrewRustInPath() { - if (!Platform.isMacOS) { - return false; - } - final envPath = Platform.environment['PATH'] ?? ''; - final paths = envPath.split(':'); - return paths.any((p) { - return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); - }); - } -} - -String _resolveExecutable(String executable) { - if (executable == 'rustup') { - final resolved = Rustup.executablePath(); - if (resolved != null) { - return resolved; - } - throw RustupNotFoundException(); - } else { - return executable; - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/lib/src/verify_binaries.dart b/crates/sbm_ffi/cargokit/build_tool/lib/src/verify_binaries.dart deleted file mode 100644 index 4e212e2f29..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/lib/src/verify_binaries.dart +++ /dev/null @@ -1,85 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin -library; - -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; - -import 'artifacts_provider.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; - -class VerifyBinaries { - VerifyBinaries({ - required this.manifestDir, - }); - - final String manifestDir; - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final config = CargokitCrateOptions.load(manifestDir: manifestDir); - final precompiledBinaries = config.precompiledBinaries; - if (precompiledBinaries == null) { - stdout.writeln('Crate does not support precompiled binaries.'); - } else { - final crateHash = CrateHash.compute(manifestDir); - stdout.writeln('Crate hash: $crateHash'); - - for (final target in Target.all) { - final message = 'Checking ${target.rust}...'; - stdout.write(message.padRight(40)); - stdout.flush(); - - final artifacts = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - final prefix = precompiledBinaries.uriPrefix; - - bool ok = true; - - for (final artifact in artifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = - Uri.parse('$prefix$crateHash/$signatureFileName'); - - final signature = await get(signatureUrl); - if (signature.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - final asset = await get(url); - if (asset.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - - if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, - signature.bodyBytes)) { - stdout.writeln('INVALID SIGNATURE'); - ok = false; - } - } - - if (ok) { - stdout.writeln('OK'); - } - } - } - } -} diff --git a/crates/sbm_ffi/cargokit/build_tool/pubspec.lock b/crates/sbm_ffi/cargokit/build_tool/pubspec.lock deleted file mode 100644 index 343bdd3694..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/pubspec.lock +++ /dev/null @@ -1,453 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 - url: "https://pub.dev" - source: hosted - version: "64.0.0" - adaptive_number: - dependency: transitive - description: - name: adaptive_number - sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - args: - dependency: "direct main" - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: "direct main" - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" - url: "https://pub.dev" - source: hosted - version: "1.6.3" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - ed25519_edwards: - dependency: "direct main" - description: - name: ed25519_edwards - sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - file: - dependency: transitive - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - github: - dependency: "direct main" - description: - name: github - sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" - url: "https://pub.dev" - source: hosted - version: "9.17.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - hex: - dependency: "direct main" - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - http: - dependency: "direct main" - description: - name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - lints: - dependency: "direct dev" - description: - name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - logging: - dependency: "direct main" - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - meta: - dependency: transitive - description: - name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: "direct main" - description: - name: path - sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 - url: "https://pub.dev" - source: hosted - version: "5.4.0" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" - source_span: - dependency: "direct main" - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" - url: "https://pub.dev" - source: hosted - version: "1.24.6" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - test_core: - dependency: transitive - description: - name: test_core - sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" - url: "https://pub.dev" - source: hosted - version: "0.5.6" - toml: - dependency: "direct main" - description: - name: toml - sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" - url: "https://pub.dev" - source: hosted - version: "0.14.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - version: - dependency: "direct main" - description: - name: version - sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" - url: "https://pub.dev" - source: hosted - version: "11.9.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.0.0 <4.0.0" diff --git a/crates/sbm_ffi/cargokit/build_tool/pubspec.yaml b/crates/sbm_ffi/cargokit/build_tool/pubspec.yaml deleted file mode 100644 index 18c61e3386..0000000000 --- a/crates/sbm_ffi/cargokit/build_tool/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# This is copied from Cargokit (which is the official way to use it currently) -# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -name: build_tool -description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. -publish_to: none -version: 1.0.0 - -environment: - sdk: ">=3.0.0 <4.0.0" - -# Add regular dependencies here. -dependencies: - # these are pinned on purpose because the bundle_tool_runner doesn't have - # pubspec.lock. See run_build_tool.sh - logging: 1.2.0 - path: 1.8.0 - version: 3.0.0 - collection: 1.18.0 - ed25519_edwards: 0.3.1 - hex: 0.2.0 - yaml: 3.1.2 - source_span: 1.10.0 - github: 9.17.0 - args: 2.4.2 - crypto: 3.0.3 - convert: 3.1.1 - http: 1.1.0 - toml: 0.14.0 - -dev_dependencies: - lints: ^2.1.0 - test: ^1.24.0 diff --git a/crates/sbm_ffi/cargokit/cmake/cargokit.cmake b/crates/sbm_ffi/cargokit/cmake/cargokit.cmake deleted file mode 100644 index ddd05df9b4..0000000000 --- a/crates/sbm_ffi/cargokit/cmake/cargokit.cmake +++ /dev/null @@ -1,99 +0,0 @@ -SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) - -if(WIN32) - # REALPATH does not properly resolve symlinks on windows :-/ - execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) -endif() - -# Arguments -# - target: CMAKE target to which rust library is linked -# - manifest_dir: relative path from current folder to directory containing cargo manifest -# - lib_name: cargo package name -# - any_symbol_name: name of any exported symbol from the library. -# used on windows to force linking with library. -function(apply_cargokit target manifest_dir lib_name any_symbol_name) - - set(CARGOKIT_LIB_NAME "${lib_name}") - set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") - if (CMAKE_CONFIGURATION_TYPES) - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") - else() - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") - endif() - set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") - - if (FLUTTER_TARGET_PLATFORM) - set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") - else() - set(CARGOKIT_TARGET_PLATFORM "windows-x64") - endif() - - set(CARGOKIT_ENV - "CARGOKIT_CMAKE=${CMAKE_COMMAND}" - "CARGOKIT_CONFIGURATION=$" - "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" - "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" - "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" - "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" - "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" - "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" - ) - - if (WIN32) - set(SCRIPT_EXTENSION ".cmd") - set(IMPORT_LIB_EXTENSION ".lib") - else() - set(SCRIPT_EXTENSION ".sh") - set(IMPORT_LIB_EXTENSION "") - execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") - endif() - - # Using generators in custom command is only supported in CMake 3.20+ - if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") - foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) - add_custom_command( - OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endforeach() - else() - add_custom_command( - OUTPUT - ${OUTPUT_LIB} - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endif() - - - set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) - - if (TARGET ${target}) - # If we have actual cmake target provided create target and make existing - # target depend on it - add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) - add_dependencies("${target}" "${target}_cargokit") - target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") - if(WIN32) - target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") - endif() - else() - # Otherwise (FFI) just use ALL to force building always - add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) - endif() - - # Allow adding the output library to plugin bundled libraries - set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) - -endfunction() diff --git a/crates/sbm_ffi/cargokit/cmake/resolve_symlinks.ps1 b/crates/sbm_ffi/cargokit/cmake/resolve_symlinks.ps1 deleted file mode 100644 index 2ac593a1ff..0000000000 --- a/crates/sbm_ffi/cargokit/cmake/resolve_symlinks.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -function Resolve-Symlinks { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [string] $Path - ) - - [string] $separator = '/' - [string[]] $parts = $Path.Split($separator) - - [string] $realPath = '' - foreach ($part in $parts) { - if ($realPath -and !$realPath.EndsWith($separator)) { - $realPath += $separator - } - - $realPath += $part.Replace('\', '/') - - # The slash is important when using Get-Item on Drive letters in pwsh. - if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) { - $realPath += '/' - } - - $item = Get-Item $realPath - if ($item.LinkTarget) { - $realPath = $item.LinkTarget.Replace('\', '/') - } - } - $realPath -} - -$path = Resolve-Symlinks -Path $args[0] -Write-Host $path diff --git a/crates/sbm_ffi/cargokit/gradle/plugin.gradle b/crates/sbm_ffi/cargokit/gradle/plugin.gradle deleted file mode 100644 index 68ff64991f..0000000000 --- a/crates/sbm_ffi/cargokit/gradle/plugin.gradle +++ /dev/null @@ -1,184 +0,0 @@ -/// This is copied from Cargokit (which is the official way to use it currently) -/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin - -import java.nio.file.Paths -import org.apache.tools.ant.taskdefs.condition.Os - -CargoKitPlugin.file = buildscript.sourceFile - -apply plugin: CargoKitPlugin - -class CargoKitExtension { - String manifestDir; // Relative path to folder containing Cargo.toml - String libname; // Library name within Cargo.toml. Must be a cdylib -} - -abstract class CargoKitBuildTask extends DefaultTask { - - @Input - String buildMode - - @Input - String buildDir - - @Input - String outputDir - - @Input - String ndkVersion - - @Input - String sdkDirectory - - @Input - int compileSdkVersion; - - @Input - int minSdkVersion; - - @Input - String pluginFile - - @Input - List targetPlatforms - - @TaskAction - def build() { - if (project.cargokit.manifestDir == null) { - throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); - } - - if (project.cargokit.libname == null) { - throw new GradleException("Property 'libname' must be set on cargokit extension"); - } - - def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" - def path = Paths.get(new File(pluginFile).parent, "..", executableName); - - def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) - - def rootProjectDir = project.rootProject.projectDir - - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - project.exec { - commandLine 'chmod', '+x', path - } - } - - project.exec { - executable path - args "build-gradle" - environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir - environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" - environment "CARGOKIT_MANIFEST_DIR", manifestDir - environment "CARGOKIT_CONFIGURATION", buildMode - environment "CARGOKIT_TARGET_TEMP_DIR", buildDir - environment "CARGOKIT_OUTPUT_DIR", outputDir - environment "CARGOKIT_NDK_VERSION", ndkVersion - environment "CARGOKIT_SDK_DIR", sdkDirectory - environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion - environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion - environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") - environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] - } - } -} - -class CargoKitPlugin implements Plugin { - - static String file; - - private Plugin findFlutterPlugin(Project rootProject) { - _findFlutterPlugin(rootProject.childProjects) - } - - private Plugin _findFlutterPlugin(Map projects) { - for (project in projects) { - for (plugin in project.value.getPlugins()) { - if (plugin.class.name == "com.flutter.gradle.FlutterPlugin" || plugin.class.name == "FlutterPlugin") { - return plugin; - } - } - def plugin = _findFlutterPlugin(project.value.childProjects); - if (plugin != null) { - return plugin; - } - } - return null; - } - - @Override - void apply(Project project) { - def plugin = findFlutterPlugin(project.rootProject); - - project.extensions.create("cargokit", CargoKitExtension) - - if (plugin == null) { - print("Flutter plugin not found, CargoKit plugin will not be applied.") - return; - } - - def cargoBuildDir = "${project.buildDir}/build" - - // Determine if the project is an application or library - def isApplication = plugin.project.plugins.hasPlugin('com.android.application') - def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants - - variants.all { variant -> - - final buildType = variant.buildType.name - - def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; - def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; - jniLibs.srcDir(new File(cargoOutputDir)) - - def List platforms - try { - platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() - } catch (Exception ignored) { - platforms = plugin.getTargetPlatforms().collect() - } - - // Same thing addFlutterDependencies does in flutter.gradle - if (buildType == "debug") { - platforms.add("android-x86") - platforms.add("android-x64") - } - - // The task name depends on plugin properties, which are not available - // at this point - project.getGradle().afterProject { - def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; - - if (project.tasks.findByName(taskName)) { - return - } - - if (plugin.project.android.ndkVersion == null) { - throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") - } - - def task = project.tasks.create(taskName, CargoKitBuildTask.class) { - buildMode = variant.buildType.name - buildDir = cargoBuildDir - outputDir = cargoOutputDir - ndkVersion = plugin.project.android.ndkVersion - sdkDirectory = plugin.project.android.sdkDirectory - minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int - compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int - targetPlatforms = platforms - pluginFile = CargoKitPlugin.file - } - def onTask = { newTask -> - if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { - newTask.dependsOn task - // Fix gradle 7.4.2 not picking up JNI library changes - newTask.outputs.upToDateWhen { false } - } - } - project.tasks.each onTask - project.tasks.whenTaskAdded onTask - } - } - } -} diff --git a/crates/sbm_ffi/cargokit/run_build_tool.cmd b/crates/sbm_ffi/cargokit/run_build_tool.cmd deleted file mode 100755 index c45d0aa8b5..0000000000 --- a/crates/sbm_ffi/cargokit/run_build_tool.cmd +++ /dev/null @@ -1,91 +0,0 @@ -@echo off -setlocal - -setlocal ENABLEDELAYEDEXPANSION - -SET BASEDIR=%~dp0 - -if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( - mkdir "%CARGOKIT_TOOL_TEMP_DIR%" -) -cd /D "%CARGOKIT_TOOL_TEMP_DIR%" - -SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool -SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart - -set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% - -( - echo name: build_tool_runner - echo version: 1.0.0 - echo publish_to: none - echo. - echo environment: - echo sdk: '^>=3.0.0 ^<4.0.0' - echo. - echo dependencies: - echo build_tool: - echo path: %BUILD_TOOL_PKG_DIR_POSIX% -) >pubspec.yaml - -if not exist bin ( - mkdir bin -) - -( - echo import 'package:build_tool/build_tool.dart' as build_tool; - echo void main^(List^ args^) ^{ - echo build_tool.runMain^(args^); - echo ^} -) >bin\build_tool_runner.dart - -SET PRECOMPILED=bin\build_tool_runner.dill - -REM To detect changes in package we compare output of DIR /s (recursive) -set PREV_PACKAGE_INFO=.dart_tool\package_info.prev -set CUR_PACKAGE_INFO=.dart_tool\package_info.cur - -DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" - -REM Last line in dir output is free space on harddrive. That is bound to -REM change between invocation so we need to remove it -( - Set "Line=" - For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( - SetLocal EnableDelayedExpansion - If Defined Line Echo !Line! - EndLocal - Set "Line=%%A") -) >"%CUR_PACKAGE_INFO%" -DEL "%CUR_PACKAGE_INFO%_orig" - -REM Compare current directory listing with previous -FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 - -If %ERRORLEVEL% neq 0 ( - REM Changed - copy current to previous and remove precompiled kernel - if exist "%PREV_PACKAGE_INFO%" ( - DEL "%PREV_PACKAGE_INFO%" - ) - MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" - if exist "%PRECOMPILED%" ( - DEL "%PRECOMPILED%" - ) -) - -REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% -REM which means we need to do pub get and precompile -if not exist "%PRECOMPILED%" ( - echo Running pub get in "%cd%" - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart -) - -"%DART%" "%PRECOMPILED%" %* - -REM 253 means invalid snapshot version. -If %ERRORLEVEL% equ 253 ( - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart - "%DART%" "%PRECOMPILED%" %* -) diff --git a/crates/sbm_ffi/cargokit/run_build_tool.sh b/crates/sbm_ffi/cargokit/run_build_tool.sh deleted file mode 100755 index 24b0ed89da..0000000000 --- a/crates/sbm_ffi/cargokit/run_build_tool.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash - -set -e - -BASEDIR=$(dirname "$0") - -mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" - -cd "$CARGOKIT_TOOL_TEMP_DIR" - -# Write a very simple bin package in temp folder that depends on build_tool package -# from Cargokit. This is done to ensure that we don't pollute Cargokit folder -# with .dart_tool contents. - -BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" - -if [[ -z $FLUTTER_ROOT ]]; then # not defined - DART=dart -else - DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" -fi - -cat << EOF > "pubspec.yaml" -name: build_tool_runner -version: 1.0.0 -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - build_tool: - path: "$BUILD_TOOL_PKG_DIR" -EOF - -mkdir -p "bin" - -cat << EOF > "bin/build_tool_runner.dart" -import 'package:build_tool/build_tool.dart' as build_tool; -void main(List args) { - build_tool.runMain(args); -} -EOF - -# Create alias for `shasum` if it does not exist and `sha1sum` exists -if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then - shopt -s expand_aliases - alias shasum="sha1sum" -fi - -# Dart run will not cache any package that has a path dependency, which -# is the case for our build_tool_runner. So instead we precompile the package -# ourselves. -# To invalidate the cached kernel we use the hash of ls -LR of the build_tool -# package directory. This should be good enough, as the build_tool package -# itself is not meant to have any path dependencies. - -if [[ "$OSTYPE" == "darwin"* ]]; then - PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) -else - PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) -fi - -PACKAGE_HASH_FILE=".package_hash" - -if [ -f "$PACKAGE_HASH_FILE" ]; then - EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") - if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then - rm "$PACKAGE_HASH_FILE" - fi -fi - -# Run pub get if needed. -if [ ! -f "$PACKAGE_HASH_FILE" ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" -fi - -# Rebuild the tool if it was deleted by Android Studio -if [ ! -f "bin/build_tool_runner.dill" ]; then - "$DART" compile kernel bin/build_tool_runner.dart -fi - -set +e - -"$DART" bin/build_tool_runner.dill "$@" - -exit_code=$? - -# 253 means invalid snapshot version. -if [ $exit_code == 253 ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - "$DART" bin/build_tool_runner.dill "$@" - exit_code=$? -fi - -exit $exit_code diff --git a/crates/sbm_ffi/ios/Classes/dummy_file.c b/crates/sbm_ffi/ios/Classes/dummy_file.c deleted file mode 100644 index e06dab9968..0000000000 --- a/crates/sbm_ffi/ios/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/crates/sbm_ffi/ios/sbm_ffi.podspec b/crates/sbm_ffi/ios/sbm_ffi.podspec deleted file mode 100644 index 4063ce577f..0000000000 --- a/crates/sbm_ffi/ios/sbm_ffi.podspec +++ /dev/null @@ -1,45 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint sbm_ffi.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'sbm_ffi' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '11.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' - - s.script_phase = { - :name => 'Build Rust library', - # First argument is relative path to the `rust` folder, second is name of rust library - :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" .. sbm_ffi', - :execution_position => :before_compile, - :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], - # Let XCode know that the static library referenced in -force_load below is - # created by this build step. - :output_files => ["${BUILT_PRODUCTS_DIR}/libsbm_ffi.a"], - } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Flutter.framework does not contain a i386 slice. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libsbm_ffi.a', - } -end \ No newline at end of file diff --git a/crates/sbm_ffi/linux/CMakeLists.txt b/crates/sbm_ffi/linux/CMakeLists.txt deleted file mode 100644 index 6efc4233b9..0000000000 --- a/crates/sbm_ffi/linux/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# The Flutter tooling requires that developers have CMake 3.10 or later -# installed. You should not increase this version, as doing so will cause -# the plugin to fail to compile for some customers of the plugin. -cmake_minimum_required(VERSION 3.10) - -# Project-level configuration. -set(PROJECT_NAME "sbm_ffi") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} .. sbm_ffi "") - -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(sbm_ffi_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/crates/sbm_ffi/macos/Classes/dummy_file.c b/crates/sbm_ffi/macos/Classes/dummy_file.c deleted file mode 100644 index e06dab9968..0000000000 --- a/crates/sbm_ffi/macos/Classes/dummy_file.c +++ /dev/null @@ -1 +0,0 @@ -// This is an empty file to force CocoaPods to create a framework. diff --git a/crates/sbm_ffi/macos/sbm_ffi.podspec b/crates/sbm_ffi/macos/sbm_ffi.podspec deleted file mode 100644 index 32455ad588..0000000000 --- a/crates/sbm_ffi/macos/sbm_ffi.podspec +++ /dev/null @@ -1,48 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint sbm_ffi.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'sbm_ffi' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - # The app's own floor (macos/Podfile, MACOSX_DEPLOYMENT_TARGET). CocoaPods - # raises this for us at build time, so a lower number changes nothing that - # ships — it only tells anyone reading the podspec that the Rust half is - # built against a macOS nobody supports. - s.platform = :osx, '12.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' - - s.script_phase = { - :name => 'Build Rust library', - # First argument is relative path to the `rust` folder, second is name of rust library - :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" .. sbm_ffi', - :execution_position => :before_compile, - :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], - # Let XCode know that the static library referenced in -force_load below is - # created by this build step. - :output_files => ["${BUILT_PRODUCTS_DIR}/libsbm_ffi.a"], - } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Flutter.framework does not contain a i386 slice. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libsbm_ffi.a', - } -end \ No newline at end of file diff --git a/crates/sbm_ffi/pubspec.yaml b/crates/sbm_ffi/pubspec.yaml deleted file mode 100644 index 6b69bae560..0000000000 --- a/crates/sbm_ffi/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: sbm_ffi -description: "Utility to build Rust code" -version: 0.0.1 -publish_to: none - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - ffi: ^2.0.2 - ffigen: ^11.0.0 - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - ffiPlugin: true - ios: - ffiPlugin: true - linux: - ffiPlugin: true - macos: - ffiPlugin: true - windows: - ffiPlugin: true diff --git a/crates/sbm_ffi/rust-toolchain.toml b/crates/sbm_ffi/rust-toolchain.toml new file mode 100644 index 0000000000..f4da8a8c6d --- /dev/null +++ b/crates/sbm_ffi/rust-toolchain.toml @@ -0,0 +1,26 @@ +# `native_toolchain_rust` wants an exact channel rather than `stable`, so that +# the library shipped in a release is built by a toolchain the version control +# names. +# +# Scoped to this crate rather than the workspace root: `sbm_parser`, +# `sbm_native` and `monitor` are built by cargo directly and have no reason to +# be pinned to whatever the app's build hook happens to need. +# +# `targets` is every platform the app ships to. A target missing here is not a +# build error, it is a build that silently falls back to the host. +[toolchain] +channel = "1.97.1" +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-apple-ios", + "aarch64-apple-ios-sim", + "x86_64-apple-ios", + "aarch64-linux-android", + "armv7-linux-androideabi", + "x86_64-linux-android", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "aarch64-pc-windows-msvc", + "x86_64-pc-windows-msvc", +] diff --git a/crates/sbm_ffi/src/frb_generated.rs b/crates/sbm_ffi/src/frb_generated.rs index e2bab9555a..76ffd56ca0 100644 --- a/crates/sbm_ffi/src/frb_generated.rs +++ b/crates/sbm_ffi/src/frb_generated.rs @@ -1,5 +1,5 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. #![allow( non_camel_case_types, @@ -21,24 +21,26 @@ clippy::explicit_auto_deref, clippy::borrow_deref_ref, clippy::uninlined_format_args, - clippy::needless_borrow + clippy::needless_borrow, + mismatched_lifetime_syntaxes )] // Section: imports -use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; -use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; use flutter_rust_bridge::{Handler, IntoIntoDart}; +use flutter_rust_bridge::for_generated::{Lockable, transform_result_dco, Lifetimeable}; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, WriteBytesExt, ReadBytesExt}; // Section: boilerplate flutter_rust_bridge::frb_generated_boilerplate!( - default_stream_sink_codec = SseCodec, - default_rust_opaque = RustOpaqueMoi, - default_rust_auto_opaque = RustAutoOpaqueMoi, -); -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -397184763; + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, + ); + pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0-beta.6"; + pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -397184763; + // Section: executor @@ -46,1003 +48,543 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs -fn wire__crate__api__script__build_script_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "build_script", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); +fn wire__crate__api__script__build_script_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "build_script", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_system = ::sse_decode(&mut deserializer); - let api_disabled = >::sse_decode(&mut deserializer); - let api_build_number = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = - crate::api::script::build_script(api_system, api_disabled, api_build_number)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__parser__command_specs_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "command_specs", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_system = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::parser::command_specs(api_system)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__exec_command_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "exec_command", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); +let api_disabled = >::sse_decode(&mut deserializer); +let api_build_number = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::script::build_script(api_system, api_disabled, api_build_number)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__parser__command_specs_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "command_specs", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_system = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::parser::command_specs(api_system)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__exec_command_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "exec_command", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_system = ::sse_decode(&mut deserializer); - let api_script_path = ::sse_decode(&mut deserializer); - let api_func = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = - crate::api::script::exec_command(api_system, api_script_path, api_func)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__parser__init_app_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "init_app", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - deserializer.end(); - move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok({ - crate::api::parser::init_app(); - })?; - Ok(output_ok) - })()) - } - }, - ) -} -fn wire__crate__api__script__install_command_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "install_command", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); +let api_script_path = ::sse_decode(&mut deserializer); +let api_func = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::script::exec_command(api_system, api_script_path, api_func)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__parser__init_app_impl(port_: flutter_rust_bridge::for_generated::MessagePort,ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "init_app", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>({ crate::api::parser::init_app(); })?; std::result::Result::Ok(output_ok) + })()) + } }) + }fn wire__crate__api__script__install_command_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "install_command", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_system = ::sse_decode(&mut deserializer); - let api_script_dir = ::sse_decode(&mut deserializer); - let api_script_path = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::script::install_command( - api_system, - api_script_dir, - api_script_path, - )?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__install_custom_cmds_command_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "install_custom_cmds_command", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); +let api_script_dir = ::sse_decode(&mut deserializer); +let api_script_path = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::script::install_command(api_system, api_script_dir, api_script_path)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__install_custom_cmds_command_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "install_custom_cmds_command", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_system = ::sse_decode(&mut deserializer); - let api_cmds = >::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = - crate::api::script::install_custom_cmds_command(api_system, api_cmds)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__install_payload_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "install_payload", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); +let api_cmds = >::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::script::install_custom_cmds_command(api_system, api_cmds)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__install_payload_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "install_payload", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_system = ::sse_decode(&mut deserializer); - let api_content = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::script::install_payload(api_system, api_content)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__parse_custom_cmds_listing_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_custom_cmds_listing", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_raw = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::script::parse_custom_cmds_listing(api_raw))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__parse_script_segments_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_script_segments", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_raw = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| { +let api_content = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::script::install_payload(api_system, api_content)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__parse_custom_cmds_listing_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "parse_custom_cmds_listing", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_raw = ::sse_decode(&mut deserializer);deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::script::parse_script_segments(api_raw))?; - Ok(output_ok) - })()) - } - }, - ) -} -fn wire__crate__api__parser__parse_status_json_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_status_json", - port: Some(port_), - mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let output_ok = Ok::<_, ()>(crate::api::script::parse_custom_cmds_listing(api_raw))?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__parse_script_segments_impl(port_: flutter_rust_bridge::for_generated::MessagePort,ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "parse_script_segments", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_raw = ::sse_decode(&mut deserializer);deserializer.end(); move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::script::parse_script_segments(api_raw))?; std::result::Result::Ok(output_ok) + })()) + } }) + }fn wire__crate__api__parser__parse_status_json_impl(port_: flutter_rust_bridge::for_generated::MessagePort,ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "parse_status_json", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_system = ::sse_decode(&mut deserializer); - let api_raw = - >::sse_decode(&mut deserializer); - let api_temp_divisor = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| { +let api_raw = >::sse_decode(&mut deserializer); +let api_temp_divisor = ::sse_decode(&mut deserializer);deserializer.end(); move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::parser::parse_status_json(api_system, api_raw, api_temp_divisor)?; std::result::Result::Ok(output_ok) + })()) + } }) + }fn wire__crate__api__parser__parse_windows_net_speed_json_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "parse_windows_net_speed_json", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_raw = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::parser::parse_windows_net_speed_json(api_raw))?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__read_custom_cmds_command_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "read_custom_cmds_command", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_system = ::sse_decode(&mut deserializer);deserializer.end(); transform_result_sse::<_, String>((move || { - let output_ok = crate::api::parser::parse_status_json( - api_system, - api_raw, - api_temp_divisor, - )?; - Ok(output_ok) - })()) - } - }, - ) -} -fn wire__crate__api__parser__parse_windows_net_speed_json_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_windows_net_speed_json", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_raw = ::sse_decode(&mut deserializer); + let output_ok = crate::api::script::read_custom_cmds_command(api_system)?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__parser__separator_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "separator", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::parser::parse_windows_net_speed_json(api_raw))?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__read_custom_cmds_command_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "read_custom_cmds_command", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_system = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::script::read_custom_cmds_command(api_system)?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__parser__separator_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "separator", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::parser::separator())?; - Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__script__shell_func_flag_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "shell_func_flag", - port: None, - mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, - }, - move || { - let message = unsafe { - flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( - ptr_, - rust_vec_len_, - data_len_, - ) - }; - let mut deserializer = - flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_func = ::sse_decode(&mut deserializer); - deserializer.end(); - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::script::shell_func_flag(api_func))?; - Ok(output_ok) - })()) - }, - ) -} + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::parser::separator())?; std::result::Result::Ok(output_ok) + })()) }) + }fn wire__crate__api__script__shell_func_flag_impl(ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,rust_vec_len_: i32,data_len_: i32) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "shell_func_flag", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_func = ::sse_decode(&mut deserializer);deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::script::shell_func_flag(api_func))?; std::result::Result::Ok(output_ok) + })()) }) + } // Section: dart2rust -impl SseDecode for std::collections::HashMap { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return inner.into_iter().collect(); - } -} - -impl SseDecode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return String::from_utf8(inner).unwrap(); - } -} - -impl SseDecode for crate::api::parser::CommandSpec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_key = ::sse_decode(deserializer); - let mut var_cmd = ::sse_decode(deserializer); - return crate::api::parser::CommandSpec { - key: var_key, - cmd: var_cmd, - }; - } -} -impl SseDecode for crate::api::script::CustomCmd { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_name = ::sse_decode(deserializer); - let mut var_cmd = ::sse_decode(deserializer); - return crate::api::script::CustomCmd { - name: var_name, - cmd: var_cmd, - }; - } -} - -impl SseDecode for f64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_f64::().unwrap() - } -} - -impl SseDecode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i32::().unwrap() - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); + impl SseDecode for std::collections::HashMap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut inner = >::sse_decode(deserializer); + return inner.into_iter().collect();} + } + + impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap();} + } + + impl SseDecode for crate::api::parser::CommandSpec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut var_key = ::sse_decode(deserializer); +let mut var_cmd = ::sse_decode(deserializer); +return crate::api::parser::CommandSpec{key: var_key, cmd: var_cmd};} + } + + impl SseDecode for crate::api::script::CustomCmd { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut var_name = ::sse_decode(deserializer); +let mut var_cmd = ::sse_decode(deserializer); +return crate::api::script::CustomCmd{name: var_name, cmd: var_cmd};} + } + + impl SseDecode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {deserializer.cursor.read_f64::().unwrap()} + } + + impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {deserializer.cursor.read_i32::().unwrap()} + } + + impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut len_ = ::sse_decode(deserializer); let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); + for idx_ in 0..len_ { ans_.push(::sse_decode(deserializer)); } + return ans_;} + } + + impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut len_ = ::sse_decode(deserializer); let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); + for idx_ in 0..len_ { ans_.push(::sse_decode(deserializer)); } + return ans_;} + } + + impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut len_ = ::sse_decode(deserializer); let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); + for idx_ in 0..len_ { ans_.push(::sse_decode(deserializer)); } + return ans_;} + } + + impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut len_ = ::sse_decode(deserializer); let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec<(String, String)> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); + for idx_ in 0..len_ { ans_.push(::sse_decode(deserializer)); } + return ans_;} + } + + impl SseDecode for Vec<(String,String,)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut len_ = ::sse_decode(deserializer); let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(<(String, String)>::sse_decode(deserializer)); - } - return ans_; - } -} - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); + for idx_ in 0..len_ { ans_.push(<(String,String,)>::sse_decode(deserializer)); } + return ans_;} + } + + impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut len_ = ::sse_decode(deserializer); let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode( - deserializer, - )); - } - return ans_; - } -} - -impl SseDecode for Option> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(>::sse_decode( - deserializer, - )); - } else { - return None; - } - } -} - -impl SseDecode for (String, String) { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_field0 = ::sse_decode(deserializer); - let mut var_field1 = ::sse_decode(deserializer); - return (var_field0, var_field1); - } -} - -impl SseDecode for crate::api::script::ScriptSegment { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_key = ::sse_decode(deserializer); - let mut var_value = ::sse_decode(deserializer); - return crate::api::script::ScriptSegment { - key: var_key, - value: var_value, - }; - } -} - -impl SseDecode for crate::api::script::ShellFuncKind { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); + for idx_ in 0..len_ { ans_.push(::sse_decode(deserializer)); } + return ans_;} + } + + impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + }} + } + + impl SseDecode for (String,String,) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut var_field0 = ::sse_decode(deserializer); +let mut var_field1 = ::sse_decode(deserializer); +return (var_field0, var_field1);} + } + + impl SseDecode for crate::api::script::ScriptSegment { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut var_key = ::sse_decode(deserializer); +let mut var_value = ::sse_decode(deserializer); +return crate::api::script::ScriptSegment{key: var_key, value: var_value};} + } + + impl SseDecode for crate::api::script::ShellFuncKind { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {let mut inner = ::sse_decode(deserializer); return match inner { 0 => crate::api::script::ShellFuncKind::Status, - 1 => crate::api::script::ShellFuncKind::StatusExt, - 2 => crate::api::script::ShellFuncKind::Process, - 3 => crate::api::script::ShellFuncKind::Shutdown, - 4 => crate::api::script::ShellFuncKind::Reboot, - 5 => crate::api::script::ShellFuncKind::Suspend, +1 => crate::api::script::ShellFuncKind::StatusExt, +2 => crate::api::script::ShellFuncKind::Process, +3 => crate::api::script::ShellFuncKind::Shutdown, +4 => crate::api::script::ShellFuncKind::Reboot, +5 => crate::api::script::ShellFuncKind::Suspend, _ => unreachable!("Invalid variant for ShellFuncKind: {}", inner), - }; - } -} - -impl SseDecode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() - } -} - -impl SseDecode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} -} - -impl SseDecode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() != 0 - } -} - -fn pde_ffi_dispatcher_primary_impl( - func_id: i32, - port: flutter_rust_bridge::for_generated::MessagePort, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 4 => wire__crate__api__parser__init_app_impl(port, ptr, rust_vec_len, data_len), - 9 => { - wire__crate__api__script__parse_script_segments_impl(port, ptr, rust_vec_len, data_len) - } - 10 => wire__crate__api__parser__parse_status_json_impl(port, ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} - -fn pde_ffi_dispatcher_sync_impl( - func_id: i32, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 1 => wire__crate__api__script__build_script_impl(ptr, rust_vec_len, data_len), - 2 => wire__crate__api__parser__command_specs_impl(ptr, rust_vec_len, data_len), - 3 => wire__crate__api__script__exec_command_impl(ptr, rust_vec_len, data_len), - 5 => wire__crate__api__script__install_command_impl(ptr, rust_vec_len, data_len), - 6 => { - wire__crate__api__script__install_custom_cmds_command_impl(ptr, rust_vec_len, data_len) - } - 7 => wire__crate__api__script__install_payload_impl(ptr, rust_vec_len, data_len), - 8 => wire__crate__api__script__parse_custom_cmds_listing_impl(ptr, rust_vec_len, data_len), - 11 => { - wire__crate__api__parser__parse_windows_net_speed_json_impl(ptr, rust_vec_len, data_len) - } - 12 => wire__crate__api__script__read_custom_cmds_command_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__parser__separator_impl(ptr, rust_vec_len, data_len), - 14 => wire__crate__api__script__shell_func_flag_impl(ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} + };} + } + + impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {deserializer.cursor.read_u8().unwrap()} + } + + impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} + } + + impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {deserializer.cursor.read_u8().unwrap() != 0} + } + + fn pde_ffi_dispatcher_primary_impl( + func_id: i32,port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, + ) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 4 => wire__crate__api__parser__init_app_impl(port, ptr, rust_vec_len, data_len), +9 => wire__crate__api__script__parse_script_segments_impl(port, ptr, rust_vec_len, data_len), +10 => wire__crate__api__parser__parse_status_json_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } + } + + fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, + ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__script__build_script_impl(ptr, rust_vec_len, data_len), +2 => wire__crate__api__parser__command_specs_impl(ptr, rust_vec_len, data_len), +3 => wire__crate__api__script__exec_command_impl(ptr, rust_vec_len, data_len), +5 => wire__crate__api__script__install_command_impl(ptr, rust_vec_len, data_len), +6 => wire__crate__api__script__install_custom_cmds_command_impl(ptr, rust_vec_len, data_len), +7 => wire__crate__api__script__install_payload_impl(ptr, rust_vec_len, data_len), +8 => wire__crate__api__script__parse_custom_cmds_listing_impl(ptr, rust_vec_len, data_len), +11 => wire__crate__api__parser__parse_windows_net_speed_json_impl(ptr, rust_vec_len, data_len), +12 => wire__crate__api__script__read_custom_cmds_command_impl(ptr, rust_vec_len, data_len), +13 => wire__crate__api__parser__separator_impl(ptr, rust_vec_len, data_len), +14 => wire__crate__api__script__shell_func_flag_impl(ptr, rust_vec_len, data_len), + _ => unreachable!(), + } + } + // Section: rust2dart // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::parser::CommandSpec { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.key.into_into_dart().into_dart(), - self.cmd.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::parser::CommandSpec -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::parser::CommandSpec -{ - fn into_into_dart(self) -> crate::api::parser::CommandSpec { - self - } -} + impl flutter_rust_bridge::IntoDart for crate::api::parser::CommandSpec { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.key.into_into_dart().into_dart(), +self.cmd.into_into_dart().into_dart() + ].into_dart() + } + } + impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::parser::CommandSpec {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::parser::CommandSpec { + fn into_into_dart(self) -> crate::api::parser::CommandSpec { + self + } + } // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::script::CustomCmd { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.name.into_into_dart().into_dart(), - self.cmd.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::script::CustomCmd {} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::script::CustomCmd -{ - fn into_into_dart(self) -> crate::api::script::CustomCmd { - self - } -} + impl flutter_rust_bridge::IntoDart for crate::api::script::CustomCmd { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.name.into_into_dart().into_dart(), +self.cmd.into_into_dart().into_dart() + ].into_dart() + } + } + impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::script::CustomCmd {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::script::CustomCmd { + fn into_into_dart(self) -> crate::api::script::CustomCmd { + self + } + } // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::script::ScriptSegment { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.key.into_into_dart().into_dart(), - self.value.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::script::ScriptSegment -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::script::ScriptSegment -{ - fn into_into_dart(self) -> crate::api::script::ScriptSegment { - self - } -} + impl flutter_rust_bridge::IntoDart for crate::api::script::ScriptSegment { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.key.into_into_dart().into_dart(), +self.value.into_into_dart().into_dart() + ].into_dart() + } + } + impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::script::ScriptSegment {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::script::ScriptSegment { + fn into_into_dart(self) -> crate::api::script::ScriptSegment { + self + } + } // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::api::script::ShellFuncKind { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - Self::Status => 0.into_dart(), - Self::StatusExt => 1.into_dart(), - Self::Process => 2.into_dart(), - Self::Shutdown => 3.into_dart(), - Self::Reboot => 4.into_dart(), - Self::Suspend => 5.into_dart(), - _ => unreachable!(), + impl flutter_rust_bridge::IntoDart for crate::api::script::ShellFuncKind { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Status => 0.into_dart(), +Self::StatusExt => 1.into_dart(), +Self::Process => 2.into_dart(), +Self::Shutdown => 3.into_dart(), +Self::Reboot => 4.into_dart(), +Self::Suspend => 5.into_dart(), + _ => unreachable!(), + } + } + } + impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::script::ShellFuncKind {} +impl flutter_rust_bridge::IntoIntoDart for crate::api::script::ShellFuncKind { + fn into_into_dart(self) -> crate::api::script::ShellFuncKind { + self + } } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::api::script::ShellFuncKind -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::api::script::ShellFuncKind -{ - fn into_into_dart(self) -> crate::api::script::ShellFuncKind { - self - } -} -impl SseEncode for std::collections::HashMap { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.into_iter().collect(), serializer); - } -} - -impl SseEncode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.into_bytes(), serializer); - } -} - -impl SseEncode for crate::api::parser::CommandSpec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.key, serializer); - ::sse_encode(self.cmd, serializer); - } -} + impl SseEncode for std::collections::HashMap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {>::sse_encode(self.into_iter().collect(), serializer);} + } + + impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {>::sse_encode(self.into_bytes(), serializer);} + } + + impl SseEncode for crate::api::parser::CommandSpec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.key, serializer); +::sse_encode(self.cmd, serializer);} + } + + impl SseEncode for crate::api::script::CustomCmd { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.name, serializer); +::sse_encode(self.cmd, serializer);} + } + + impl SseEncode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {serializer.cursor.write_f64::(self).unwrap();} + } + + impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {serializer.cursor.write_i32::(self).unwrap();} + } + + impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.len() as _, serializer); + for item in self { ::sse_encode(item, serializer); }} + } + + impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.len() as _, serializer); + for item in self { ::sse_encode(item, serializer); }} + } + + impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.len() as _, serializer); + for item in self { ::sse_encode(item, serializer); }} + } + + impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.len() as _, serializer); + for item in self { ::sse_encode(item, serializer); }} + } + + impl SseEncode for Vec<(String,String,)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.len() as _, serializer); + for item in self { <(String,String,)>::sse_encode(item, serializer); }} + } + + impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.len() as _, serializer); + for item in self { ::sse_encode(item, serializer); }} + } + + impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + }} + } + + impl SseEncode for (String,String,) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.0, serializer); +::sse_encode(self.1, serializer);} + } + + impl SseEncode for crate::api::script::ScriptSegment { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(self.key, serializer); +::sse_encode(self.value, serializer);} + } + + impl SseEncode for crate::api::script::ShellFuncKind { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {::sse_encode(match self {crate::api::script::ShellFuncKind::Status => { 0 } +crate::api::script::ShellFuncKind::StatusExt => { 1 } +crate::api::script::ShellFuncKind::Process => { 2 } +crate::api::script::ShellFuncKind::Shutdown => { 3 } +crate::api::script::ShellFuncKind::Reboot => { 4 } +crate::api::script::ShellFuncKind::Suspend => { 5 } + _ => { unimplemented!(""); }}, serializer);} + } + + impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {serializer.cursor.write_u8(self).unwrap();} + } + + impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} + } + + impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {serializer.cursor.write_u8(self as _).unwrap();} + } + -impl SseEncode for crate::api::script::CustomCmd { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.name, serializer); - ::sse_encode(self.cmd, serializer); - } -} -impl SseEncode for f64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_f64::(self).unwrap(); - } -} + + + #[cfg(not(target_family = "wasm"))] + mod io { + // This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. -impl SseEncode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_i32::(self).unwrap(); - } -} -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} +// Section: imports -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} +use flutter_rust_bridge::{Handler, IntoIntoDart}; +use flutter_rust_bridge::for_generated::{Lockable, transform_result_dco, Lifetimeable}; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, WriteBytesExt, ReadBytesExt};use super::*; -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} +// Section: boilerplate -impl SseEncode for Vec<(String, String)> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - <(String, String)>::sse_encode(item, serializer); - } - } -} +flutter_rust_bridge::frb_generated_boilerplate_io!(); -impl SseEncode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.len() as _, serializer); - for item in self { - ::sse_encode(item, serializer); - } - } -} -impl SseEncode for Option> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - >::sse_encode(value, serializer); } - } -} - -impl SseEncode for (String, String) { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.0, serializer); - ::sse_encode(self.1, serializer); - } -} - -impl SseEncode for crate::api::script::ScriptSegment { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.key, serializer); - ::sse_encode(self.value, serializer); - } -} - -impl SseEncode for crate::api::script::ShellFuncKind { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode( - match self { - crate::api::script::ShellFuncKind::Status => 0, - crate::api::script::ShellFuncKind::StatusExt => 1, - crate::api::script::ShellFuncKind::Process => 2, - crate::api::script::ShellFuncKind::Shutdown => 3, - crate::api::script::ShellFuncKind::Reboot => 4, - crate::api::script::ShellFuncKind::Suspend => 5, - _ => { - unimplemented!(""); - } - }, - serializer, - ); - } -} + #[cfg(not(target_family = "wasm"))] + pub use io::*; + + + /// cbindgen:ignore + #[cfg(target_family = "wasm")] + mod web { + // This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. -impl SseEncode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self).unwrap(); - } -} -impl SseEncode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} -} -impl SseEncode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_u8(self as _).unwrap(); - } -} - -#[cfg(not(target_family = "wasm"))] -mod io { - // This file is automatically generated, so please do not edit it. - // @generated by `flutter_rust_bridge`@ 2.12.0. - - // Section: imports - - use super::*; - use flutter_rust_bridge::for_generated::byteorder::{ - NativeEndian, ReadBytesExt, WriteBytesExt, - }; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; - use flutter_rust_bridge::{Handler, IntoIntoDart}; - - // Section: boilerplate +// Section: imports - flutter_rust_bridge::frb_generated_boilerplate_io!(); -} -#[cfg(not(target_family = "wasm"))] -pub use io::*; +use flutter_rust_bridge::{Handler, IntoIntoDart}; +use flutter_rust_bridge::for_generated::{Lockable, transform_result_dco, Lifetimeable}; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, WriteBytesExt, ReadBytesExt};use super::*; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; -/// cbindgen:ignore -#[cfg(target_family = "wasm")] -mod web { - // This file is automatically generated, so please do not edit it. - // @generated by `flutter_rust_bridge`@ 2.12.0. - // Section: imports +// Section: boilerplate - use super::*; - use flutter_rust_bridge::for_generated::byteorder::{ - NativeEndian, ReadBytesExt, WriteBytesExt, - }; - use flutter_rust_bridge::for_generated::wasm_bindgen; - use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; - use flutter_rust_bridge::{Handler, IntoIntoDart}; +flutter_rust_bridge::frb_generated_boilerplate_web!(); - // Section: boilerplate - flutter_rust_bridge::frb_generated_boilerplate_web!(); -} -#[cfg(target_family = "wasm")] -pub use web::*; + } + #[cfg(target_family = "wasm")] + pub use web::*; + \ No newline at end of file diff --git a/crates/sbm_ffi/windows/.gitignore b/crates/sbm_ffi/windows/.gitignore deleted file mode 100644 index b3eb2be169..0000000000 --- a/crates/sbm_ffi/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/crates/sbm_ffi/windows/CMakeLists.txt b/crates/sbm_ffi/windows/CMakeLists.txt deleted file mode 100644 index d55959f76f..0000000000 --- a/crates/sbm_ffi/windows/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# The Flutter tooling requires that developers have a version of Visual Studio -# installed that includes CMake 3.14 or later. You should not increase this -# version, as doing so will cause the plugin to fail to compile for some -# customers of the plugin. -cmake_minimum_required(VERSION 3.14) - -# Project-level configuration. -set(PROJECT_NAME "sbm_ffi") -project(${PROJECT_NAME} LANGUAGES CXX) - -include("../cargokit/cmake/cargokit.cmake") -apply_cargokit(${PROJECT_NAME} ../../../../../../crates/sbm_ffi sbm_ffi "") - -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(sbm_ffi_bundled_libraries - "${${PROJECT_NAME}_cargokit_lib}" - PARENT_SCOPE -) diff --git a/docs/src/content/docs/development/building.md b/docs/src/content/docs/development/building.md index 37f0f800b9..1ba0775810 100644 --- a/docs/src/content/docs/development/building.md +++ b/docs/src/content/docs/development/building.md @@ -48,7 +48,6 @@ dart run fl_build -p ios Requires: - macOS with Xcode -- CocoaPods - Apple Developer account for signing ### Android diff --git a/docs/src/content/docs/principles/architecture.md b/docs/src/content/docs/principles/architecture.md index 121647e11c..65c4c3da34 100644 --- a/docs/src/content/docs/principles/architecture.md +++ b/docs/src/content/docs/principles/architecture.md @@ -132,9 +132,9 @@ Flutter plugins provide platform integration: | Platform | Integration Method | |----------|-------------------| -| iOS | CocoaPods, Swift/Obj-C | +| iOS | Swift Package Manager, Swift/Obj-C | | Android | Gradle, Kotlin/Java | -| macOS | CocoaPods, Swift | +| macOS | Swift Package Manager, Swift | | Linux | CMake, C++ | | Windows | CMake, C++ | diff --git a/docs/src/content/docs/zh/development/building.md b/docs/src/content/docs/zh/development/building.md index 4cd197db3d..0ecf45211f 100644 --- a/docs/src/content/docs/zh/development/building.md +++ b/docs/src/content/docs/zh/development/building.md @@ -47,7 +47,6 @@ dart run fl_build -p ios 需要: - 安装了 Xcode 的 macOS -- CocoaPods - 用于签名的 Apple Developer 账号 ### Android diff --git a/docs/src/content/docs/zh/principles/architecture.md b/docs/src/content/docs/zh/principles/architecture.md index 19835d11a4..acbd029141 100644 --- a/docs/src/content/docs/zh/principles/architecture.md +++ b/docs/src/content/docs/zh/principles/architecture.md @@ -110,9 +110,9 @@ Flutter 插件提供平台集成: | 平台 | 集成方式 | |----------|-------------------| -| iOS | CocoaPods, Swift/Obj-C | +| iOS | Swift Package Manager, Swift/Obj-C | | Android | Gradle, Kotlin/Java | -| macOS | CocoaPods, Swift | +| macOS | Swift Package Manager, Swift | | Linux | CMake, C++ | | Windows | CMake, C++ | diff --git a/hook/build.dart b/hook/build.dart new file mode 100644 index 0000000000..2df0763e4b --- /dev/null +++ b/hook/build.dart @@ -0,0 +1,24 @@ +import 'package:flutter_rust_bridge_hooks/flutter_rust_bridge_hooks.dart'; + +/// Builds `crates/sbm_ffi` and hands the result to the Dart/Flutter SDK as a +/// code asset. +/// +/// This replaces cargokit, which drove cargo from a CocoaPods `script_phase` on +/// Apple platforms, a CMake step on Linux and Windows, and a gradle plugin on +/// Android — four integrations, one per platform, and the Apple one had no +/// Swift Package Manager equivalent: a SwiftPM build tool plugin runs in a +/// sandbox that denies writes to the project directory, so cargo cannot write +/// `target/` or `~/.cargo` from one. That mattered because CocoaPods' registry +/// goes read-only on 2026-12-02 and Flutter's fallback to it is removed some +/// time after. +/// +/// A build hook sidesteps the question rather than answering it: it is neither +/// a pod nor a Swift package, so it produces no `.podspec` and no +/// `Package.swift`, and the same file covers all five platforms. +void main(List args) async { + await build(args, (input, output) async { + await const FlutterRustBridgeNativeAssetsBuilder( + cratePath: 'crates/sbm_ffi', + ).run(input: input, output: output); + }); +} diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index b2f3789263..23e8b084b0 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,3 +1,2 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" #include "Ish.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index f4d23c9b10..23e8b084b0 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,3 +1,2 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" #include "Ish.xcconfig" diff --git a/ios/Podfile b/ios/Podfile deleted file mode 100644 index 5731366108..0000000000 --- a/ios/Podfile +++ /dev/null @@ -1,40 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '15.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index 61179edec8..0000000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,28 +0,0 @@ -PODS: - - Flutter (1.0.0) - - flutter_pty (0.0.1): - - Flutter - - sbm_ffi (0.0.1): - - Flutter - -DEPENDENCIES: - - Flutter (from `Flutter`) - - flutter_pty (from `.symlinks/plugins/flutter_pty/ios`) - - sbm_ffi (from `.symlinks/plugins/sbm_ffi/ios`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - flutter_pty: - :path: ".symlinks/plugins/flutter_pty/ios" - sbm_ffi: - :path: ".symlinks/plugins/sbm_ffi/ios" - -SPEC CHECKSUMS: - Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47 - flutter_pty: 206742da6092b413cb683de17436c16d1f3c7997 - sbm_ffi: 00a2185752f2a9d3a3f0d3ab4b4d5cb29729aa30 - -PODFILE CHECKSUM: 033fcc5039cba02092526aeac31fc4c94faa91b1 - -COCOAPODS: 1.17.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 33b40c4ac5..40bd9071e6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,11 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 5B1A0002200001000000ISH0 /* sbm_ish.c in Sources */ = {isa = PBXBuildFile; fileRef = 5B1A0001200001000000ISH0 /* sbm_ish.c */; }; 4A2DCD6B2E4B127100CF68B7 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */; }; 4A2DCD6C2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */; }; 4A2DCD6F2E4B128100CF68B7 /* TerminalLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */; }; 4A2DCD702E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */; }; + 5B1A0002200001000000ISH0 /* sbm_ish.c in Sources */ = {isa = PBXBuildFile; fileRef = 5B1A0001200001000000ISH0 /* sbm_ish.c */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 7538AEC32BB83FAB002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */; }; 7538AEC52BB83FC8002AB82A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7538AEC42BB83FC8002AB82A /* PrivacyInfo.xcprivacy */; }; @@ -21,7 +21,6 @@ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - DEB04D99AB3357C5060135CC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8181E2FE60383BB01C8D74CB /* Pods_Runner.framework */; }; E33A3E372A626DCD009744AB /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E33A3E362A626DCD009744AB /* WidgetKit.framework */; }; E33A3E392A626DCD009744AB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E33A3E382A626DCD009744AB /* SwiftUI.framework */; }; E33A3E3C2A626DCE009744AB /* StatusWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A3E3B2A626DCE009744AB /* StatusWidgetBundle.swift */; }; @@ -125,13 +124,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2026F2298051173C0B212E05 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4A2DCD692E4B127100CF68B7 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; 4A2DCD6A2E4B127100CF68B7 /* TerminalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivityAttributes.swift; sourceTree = ""; }; 4A2DCD6D2E4B128100CF68B7 /* TerminalLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivity.swift; sourceTree = ""; }; 4A2DCD6E2E4B128100CF68B7 /* TerminalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalLiveActivityAttributes.swift; sourceTree = ""; }; - 668FBB3ED2E093A4068CAF18 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5B1A0001200001000000ISH0 /* sbm_ish.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = sbm_ish.c; sourceTree = ""; }; + 5B1A0003200001000000ISH0 /* sbm_ish.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sbm_ish.h; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7538AEC22BB83FAB002AB82A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -139,8 +138,6 @@ 7538AEC62BB83FD3002AB82A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8181E2FE60383BB01C8D74CB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 92ED9819CC2FDBA815166FC2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -173,8 +170,6 @@ E39A76AB2AB9A2F70067C641 /* Info-Debug.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Debug.plist"; sourceTree = ""; }; E39A76AC2AB9A2F70067C641 /* Info-Release.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Release.plist"; sourceTree = ""; }; E39A76AD2AB9A2F70067C641 /* Info-Profile.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Profile.plist"; sourceTree = ""; }; - 5B1A0001200001000000ISH0 /* sbm_ish.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = sbm_ish.c; sourceTree = ""; }; - 5B1A0003200001000000ISH0 /* sbm_ish.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sbm_ish.h; sourceTree = ""; }; E3AE8AE92AB601DB000A6459 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; E3D26BC22B99637800D83425 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; E3D26BC32B99637900D83425 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/LaunchScreen.strings"; sourceTree = ""; }; @@ -221,9 +216,9 @@ FA7C40000000000000000004 /* WatchStatusWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStatusWidget.swift; sourceTree = ""; }; FA7C40000000000000000005 /* WatchStatusWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStatusWidgetBundle.swift; sourceTree = ""; }; FA7C40000000000000000006 /* WatchWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WatchWidget.entitlements; sourceTree = ""; }; - FA7C40000000000000000009 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FA7C40000000000000000007 /* WatchWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WatchWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; FA7C40000000000000000008 /* StatusWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StatusWidget.entitlements; sourceTree = ""; }; + FA7C40000000000000000009 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -232,7 +227,6 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - DEB04D99AB3357C5060135CC /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -262,15 +256,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 7D922B9C06363731246B6815 /* Pods */ = { + 5B1A0004200001000000ISH0 /* ish */ = { isa = PBXGroup; children = ( - 92ED9819CC2FDBA815166FC2 /* Pods-Runner.debug.xcconfig */, - 668FBB3ED2E093A4068CAF18 /* Pods-Runner.release.xcconfig */, - 2026F2298051173C0B212E05 /* Pods-Runner.profile.xcconfig */, + 5B1A0003200001000000ISH0 /* sbm_ish.h */, + 5B1A0001200001000000ISH0 /* sbm_ish.c */, ); - name = Pods; - path = Pods; + path = ish; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -295,7 +287,6 @@ FA7C40000000000000000010 /* WatchWidget */, 97C146EF1CF9000F007C117D /* Products */, D242A20E381A343934B6A7B6 /* Frameworks */, - 7D922B9C06363731246B6815 /* Pods */, ); sourceTree = ""; }; @@ -339,7 +330,6 @@ children = ( E33A3E362A626DCD009744AB /* WidgetKit.framework */, E33A3E382A626DCD009744AB /* SwiftUI.framework */, - 8181E2FE60383BB01C8D74CB /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -360,17 +350,6 @@ path = StatusWidget; sourceTree = ""; }; - FA7C40000000000000000010 /* WatchWidget */ = { - isa = PBXGroup; - children = ( - FA7C40000000000000000004 /* WatchStatusWidget.swift */, - FA7C40000000000000000005 /* WatchStatusWidgetBundle.swift */, - FA7C40000000000000000009 /* Info.plist */, - FA7C40000000000000000006 /* WatchWidget.entitlements */, - ); - path = WatchWidget; - sourceTree = ""; - }; E39515C82AB5AD62003602C1 /* WatchApp */ = { isa = PBXGroup; children = ( @@ -387,13 +366,15 @@ path = WatchApp; sourceTree = ""; }; - 5B1A0004200001000000ISH0 /* ish */ = { + FA7C40000000000000000010 /* WatchWidget */ = { isa = PBXGroup; children = ( - 5B1A0003200001000000ISH0 /* sbm_ish.h */, - 5B1A0001200001000000ISH0 /* sbm_ish.c */, + FA7C40000000000000000004 /* WatchStatusWidget.swift */, + FA7C40000000000000000005 /* WatchStatusWidgetBundle.swift */, + FA7C40000000000000000009 /* Info.plist */, + FA7C40000000000000000006 /* WatchWidget.entitlements */, ); - path = ish; + path = WatchWidget; sourceTree = ""; }; /* End PBXGroup section */ @@ -403,7 +384,6 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 63F6DF79DA2F47E8D2660970 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -413,7 +393,6 @@ E39515D52AB5AD64003602C1 /* Embed Watch Content */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F3A15C782F6E1D8B00A1C001 /* Generate Missing Framework dSYMs */, - D910A6AD158B759A8CE71D99 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -601,28 +580,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 63F6DF79DA2F47E8D2660970 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -638,23 +595,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - D910A6AD158B759A8CE71D99 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; F3A15C782F6E1D8B00A1C001 /* Generate Missing Framework dSYMs */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1086,6 +1026,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = StatusWidget/StatusWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1491; DEVELOPMENT_TEAM = BA88US33G6; @@ -1103,7 +1044,6 @@ MARKETING_VERSION = 1.0.1491; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - CODE_SIGN_ENTITLEMENTS = StatusWidget/StatusWidget.entitlements; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1126,6 +1066,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = StatusWidget/StatusWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1491; DEVELOPMENT_TEAM = BA88US33G6; @@ -1142,7 +1083,6 @@ ); MARKETING_VERSION = 1.0.1491; MTL_FAST_MATH = YES; - CODE_SIGN_ENTITLEMENTS = StatusWidget/StatusWidget.entitlements; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1163,6 +1103,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = StatusWidget/StatusWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1491; DEVELOPMENT_TEAM = BA88US33G6; @@ -1179,7 +1120,6 @@ ); MARKETING_VERSION = 1.0.1491; MTL_FAST_MATH = YES; - CODE_SIGN_ENTITLEMENTS = StatusWidget/StatusWidget.entitlements; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1200,6 +1140,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1491; DEVELOPMENT_ASSET_PATHS = ""; @@ -1216,7 +1157,6 @@ MARKETING_VERSION = 1.0.1491; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; SDKROOT = watchos; @@ -1242,6 +1182,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1491; DEVELOPMENT_ASSET_PATHS = ""; @@ -1257,7 +1198,6 @@ ); MARKETING_VERSION = 1.0.1491; MTL_FAST_MATH = YES; - CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; SDKROOT = watchos; @@ -1281,6 +1221,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1491; DEVELOPMENT_ASSET_PATHS = ""; @@ -1296,7 +1237,6 @@ ); MARKETING_VERSION = 1.0.1491; MTL_FAST_MATH = YES; - CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; SDKROOT = watchos; @@ -1335,19 +1275,19 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.1466; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd.WatchWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; WATCHOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 4d7193eeb3..0000000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,59 +0,0 @@ -{ - "pins" : [ - { - "identity" : "dkcamera", - "kind" : "remoteSourceControl", - "location" : "https://github.com/zhangao0086/DKCamera", - "state" : { - "branch" : "master", - "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96" - } - }, - { - "identity" : "dkimagepickercontroller", - "kind" : "remoteSourceControl", - "location" : "https://github.com/zhangao0086/DKImagePickerController", - "state" : { - "branch" : "4.3.9", - "revision" : "0bdfeacefa308545adde07bef86e349186335915" - } - }, - { - "identity" : "dkphotogallery", - "kind" : "remoteSourceControl", - "location" : "https://github.com/zhangao0086/DKPhotoGallery", - "state" : { - "branch" : "master", - "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d" - } - }, - { - "identity" : "sdwebimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImage", - "state" : { - "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0", - "version" : "5.21.7" - } - }, - { - "identity" : "swiftygif", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kirualex/SwiftyGif.git", - "state" : { - "revision" : "4430cbc148baa3907651d40562d96325426f409a", - "version" : "5.4.5" - } - }, - { - "identity" : "tocropviewcontroller", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TimOliver/TOCropViewController", - "state" : { - "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e", - "version" : "2.8.0" - } - } - ], - "version" : 2 -} diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc14c7..1d526a16ed 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/lib/core/chan.dart b/lib/core/chan.dart index be5c2fd5ad..f546454a61 100644 --- a/lib/core/chan.dart +++ b/lib/core/chan.dart @@ -67,7 +67,7 @@ abstract final class MethodChans { static Future syncAccessoryWidgetUrl() async { if (!isIOS) return; final id = Stores.setting.accessoryWidgetServerId.fetch(); - final spi = id.isEmpty ? null : Stores.server.get(id); + final spi = id.isEmpty ? null : Stores.server.fetchOneRaw(id); await setAccessoryWidgetUrl(spi?.monitorStatusUrl); } diff --git a/lib/core/service/watch_sync.dart b/lib/core/service/watch_sync.dart index b1b724f5e6..dbfd64876d 100644 --- a/lib/core/service/watch_sync.dart +++ b/lib/core/service/watch_sync.dart @@ -102,7 +102,7 @@ final class WatchSync { /// What the watch app is told to display. Map buildPayload() => payloadFrom( selectedIds: Stores.setting.watchServerIds.fetch(), - lookup: (id) => Stores.server.get(id), + lookup: (id) => Stores.server.fetchOneRaw(id), // TODO: drop with `SettingStore.watchLegacyUrls`. legacyUrls: Stores.setting.watchLegacyUrls.fetch(), ); @@ -209,7 +209,7 @@ final class WatchSync { }); // A server's monitor address or password can change without the watch // selection changing, and the watch would keep using the stale copy. - _serverStoreSub = Stores.server.box.watch().listen((_) => _schedulePush()); + _serverStoreSub = Stores.server.watch().listen((_) => _schedulePush()); } void _schedulePush() { diff --git a/lib/core/utils/sandbox_import.dart b/lib/core/utils/sandbox_import.dart index b7ebe51bfc..2a62730bee 100644 --- a/lib/core/utils/sandbox_import.dart +++ b/lib/core/utils/sandbox_import.dart @@ -156,7 +156,7 @@ abstract final class SandboxImport { final destNames = await _names(dest) ?? const []; if (destNames.contains(doneMarker)) return SandboxImportResult.skipped; final resuming = destNames.contains(busyMarker); - if (!resuming && destNames.any(_isBox)) return SandboxImportResult.skipped; + if (!resuming && destNames.any(_isData)) return SandboxImportResult.skipped; final List srcNames; try { @@ -176,7 +176,7 @@ abstract final class SandboxImport { return SandboxImportResult.notFound; } - if (!srcNames.any(_isBox)) return SandboxImportResult.notFound; + if (!srcNames.any(_isData)) return SandboxImportResult.notFound; // Before deciding whether the boxes are readable: an old enough install // keeps the key here, and that copy is as good as the keychain's. @@ -286,7 +286,23 @@ abstract final class SandboxImport { } } - static bool _isBox(String name) => name.endsWith('.hive'); + /// Whether [name] is a file this app keeps its data in. + /// + /// Both engines, because an install can be on either: a Hive box until it has + /// launched the build that migrates, the SQLite database afterwards. Deciding + /// "this install already has data" or "the container has data" on `.hive` + /// alone stopped being true the moment anything shipped on SQLite, and + /// `_clear` skipping the database is what made the recovery in `main.dart` + /// reopen the very file that had just failed to open. + static bool _isData(String name) => + name.endsWith('.hive') || name == SqliteDb.fileName; + + /// [_isData], plus the files SQLite keeps beside the database. + /// + /// Only for deleting. A `-wal` on its own is not data, but leaving one behind + /// next to a database that was removed is worse than removing both. + static bool _isDataOrSidecar(String name) => + _isData(name) || name.startsWith('${SqliteDb.fileName}-'); static String _parentOf(String path) { final parts = path.split(Pfs.seperator)..removeLast(); @@ -301,11 +317,7 @@ abstract final class SandboxImport { final entities = await _names(dir); if (entities == null) return; for (final name in entities) { - if (!_isBox(name) && - !name.endsWith('.lock') && - !name.startsWith('app.db')) { - continue; - } + if (!_isDataOrSidecar(name) && !name.endsWith('.lock')) continue; try { await File(dir.path.joinPath(name)).delete(); } catch (e) { @@ -372,8 +384,11 @@ abstract final class SandboxImport { // database open, not to the database. Carried across it describes a // WAL index that no longer exists, and sqlite either rebuilds it or // refuses — the first is wasted, the second is a broken app. It is - // rebuilt from `app.db-wal`, which does come across. - if (name.endsWith('.db-shm')) continue; + // rebuilt from the `-wal`, which does come across. + // + // By exact name, not by suffix: a file of the user's own that happens + // to end in `-shm` is theirs and should come across. + if (name == '${SqliteDb.fileName}-shm') continue; await entity.copy(dest.path.joinPath(name)); _copiedBytes += await entity.length(); diff --git a/lib/core/utils/server.dart b/lib/core/utils/server.dart index 784816f4bb..8bb66d0fb9 100644 --- a/lib/core/utils/server.dart +++ b/lib/core/utils/server.dart @@ -343,7 +343,7 @@ List _resolveJumpCandidates({ for (final jumpId in spi.resolvedJumpIds) { final candidate = preloadedJumpSpi?.id == jumpId ? preloadedJumpSpi - : jumpSpisById?[jumpId] ?? Stores.server.box.get(jumpId); + : jumpSpisById?[jumpId] ?? Stores.server.fetchOneRaw(jumpId); if (candidate == null || candidates.any((e) => e.id == candidate.id)) { continue; } diff --git a/lib/data/model/app/bak/backup.dart b/lib/data/model/app/bak/backup.dart index 8759bc544a..33165d8e2f 100644 --- a/lib/data/model/app/bak/backup.dart +++ b/lib/data/model/app/bak/backup.dart @@ -78,134 +78,26 @@ class Backup implements Mergeable { return; } - // Snippets - if (force) { - for (final s in snippets) { - Stores.snippet.box.put(s.name, s); - } - } else { - final nowSnippets = Stores.snippet.box.keys.toSet(); - final bakSnippets = snippets.map((e) => e.name).toSet(); - final newSnippets = bakSnippets.difference(nowSnippets); - final delSnippets = nowSnippets.difference(bakSnippets); - final updateSnippets = nowSnippets.intersection(bakSnippets); - for (final s in newSnippets) { - Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s)); - } - for (final s in delSnippets) { - Stores.snippet.box.delete(s); - } - for (final s in updateSnippets) { - Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s)); - } - } - - // ServerPrivateInfo - if (force) { - for (final s in spis) { - Stores.server.box.put(s.id, s); - } - } else { - final nowSpis = Stores.server.box.keys.toSet(); - final bakSpis = spis.map((e) => e.id).toSet(); - final newSpis = bakSpis.difference(nowSpis); - final delSpis = nowSpis.difference(bakSpis); - final updateSpis = nowSpis.intersection(bakSpis); - for (final s in newSpis) { - Stores.server.box.put(s, spis.firstWhere((e) => e.id == s)); - } - for (final s in delSpis) { - Stores.server.box.delete(s); - } - for (final s in updateSpis) { - Stores.server.box.put(s, spis.firstWhere((e) => e.id == s)); - } - } - - // PrivateKeyInfo - if (force) { - for (final s in keys) { - Stores.key.box.put(s.id, s); - } - } else { - final nowKeys = Stores.key.box.keys.toSet(); - final bakKeys = keys.map((e) => e.id).toSet(); - final newKeys = bakKeys.difference(nowKeys); - final delKeys = nowKeys.difference(bakKeys); - final updateKeys = nowKeys.intersection(bakKeys); - for (final s in newKeys) { - Stores.key.box.put(s, keys.firstWhere((e) => e.id == s)); - } - for (final s in delKeys) { - Stores.key.box.delete(s); - } - for (final s in updateKeys) { - Stores.key.box.put(s, keys.firstWhere((e) => e.id == s)); - } - } - - // History - if (force) { - Stores.history.box.putAll(history); - } else { - final nowHistory = Stores.history.box.keys.toSet(); - final bakHistory = history.keys.toSet(); - final newHistory = bakHistory.difference(nowHistory); - final delHistory = nowHistory.difference(bakHistory); - final updateHistory = nowHistory.intersection(bakHistory); - for (final s in newHistory) { - Stores.history.box.put(s, history[s]); - } - for (final s in delHistory) { - Stores.history.box.delete(s); - } - for (final s in updateHistory) { - Stores.history.box.put(s, history[s]); - } - } - - // Container - if (force) { - Stores.container.box.putAll(container); - } else { - final nowContainer = Stores.container.box.keys.toSet(); - final bakContainer = container.keys.toSet(); - final newContainer = bakContainer.difference(nowContainer); - final delContainer = nowContainer.difference(bakContainer); - final updateContainer = nowContainer.intersection(bakContainer); - for (final s in newContainer) { - Stores.container.box.put(s, container[s]); - } - for (final s in delContainer) { - Stores.container.box.delete(s); - } - for (final s in updateContainer) { - Stores.container.box.put(s, container[s]); - } - } - - // Settings - final settings_ = settings; - if (settings_ != null) { - if (force) { - Stores.setting.box.putAll(settings_); - } else { - final nowSettings = Stores.setting.box.keys.toSet(); - final bakSettings = settings_.keys.toSet(); - final newSettings = bakSettings.difference(nowSettings); - final delSettings = nowSettings.difference(bakSettings); - final updateSettings = nowSettings.intersection(bakSettings); - for (final s in newSettings) { - Stores.setting.box.put(s, settings_[s]); - } - for (final s in delSettings) { - Stores.setting.box.delete(s); - } - for (final s in updateSettings) { - Stores.setting.box.put(s, settings_[s]); - } - } - } + // One transaction for the whole merge. Per-key commits would leave a + // restore that was interrupted — process killed, device out of battery — + // with servers deleted whose replacements were never written, and no way to + // tell that had happened. + SqliteStore.transact(() { + _restoreInto( + Stores.snippet, + {for (final s in snippets) s.name: s}, + force: force, + ); + _restoreInto(Stores.server, {for (final s in spis) s.id: s}, force: force); + _restoreInto(Stores.key, {for (final s in keys) s.id: s}, force: force); + _restoreInto(Stores.history, history, force: force); + _restoreInto(Stores.container, container, force: force); + + final settings_ = settings; + if (settings_ != null) { + _restoreInto(Stores.setting, settings_, force: force); + } + }); Provider.reload(); RNodes.app.notify(); @@ -217,6 +109,41 @@ class Backup implements Mergeable { Backup.fromJson(json.decode(_diyDecrypt(raw))); } +/// Writes one section of a backup into the store it came from. +/// +/// [force] replaces what is there; otherwise the store is also made to *stop* +/// holding whatever the backup does not, which is what makes a delete on one +/// device reach another. +/// +/// Nothing here stamps `lastUpdateTs`, which is what writing straight to the +/// Hive box used to achieve. A restore is not an edit: marking every restored +/// key as changed now would leave the merged copy looking newer than the backup +/// it came from, and the next sync would push it straight back out. +/// +/// Uses `keys()` rather than every key in the store, so the internal +/// `lastUpdateTs` entry is not one of the ones deleted for being absent from +/// the backup — `getAllMap` leaves it out of the backup by the same rule, so +/// the box-level version deleted it on every non-forced merge. +void _restoreInto( + SqliteStore store, + Map incoming, { + required bool force, +}) { + if (!force) { + for (final key in store.keys().difference(incoming.keys.toSet())) { + store.remove(key, updateLastUpdateTsOnRemove: false); + } + } + for (final entry in incoming.entries) { + final value = entry.value; + if (value == null) { + store.remove(entry.key, updateLastUpdateTsOnRemove: false); + continue; + } + store.set(entry.key, value, updateLastUpdateTsOnSet: false); + } +} + String _diyEncrypt(String raw) => json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false)); diff --git a/lib/data/provider/ai/agent_session.dart b/lib/data/provider/ai/agent_session.dart index 9a95b3f18a..dd2e8308b5 100644 --- a/lib/data/provider/ai/agent_session.dart +++ b/lib/data/provider/ai/agent_session.dart @@ -178,7 +178,7 @@ class AgentSession extends _$AgentSession { AgentSessionState build() { // Watches the box, so a write this class did not make — a restored backup // — is not missed. - _conversationWatch = Stores.agentConversation.box.watch().listen((_) { + _conversationWatch = Stores.agentConversation.watch().listen((_) { state = state.copyWith(conversations: _fetchConversations()); }); ref.onDispose(() { diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index ea50ba82fd..e8d50653d7 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -4,6 +4,7 @@ import 'package:server_box/data/store/agent_conversation.dart'; import 'package:server_box/data/store/connection_stats.dart'; import 'package:server_box/data/store/container.dart'; import 'package:server_box/data/store/history.dart'; +import 'package:server_box/data/store/migrations/m003_hive_to_sqlite.dart'; import 'package:server_box/data/store/port_forward.dart'; import 'package:server_box/data/store/private_key.dart'; import 'package:server_box/data/store/server.dart'; @@ -21,27 +22,27 @@ abstract final class Stores { static HistoryStore get history => getIt(); static AgentConversationStore get agentConversation => getIt(); - // Keep the legacy box registered so existing connection stats DB files remain intact. static ConnectionStatsStore get connectionStats => getIt(); static PortForwardStore get portForward => getIt(); - /// All stores that need backup - static List get _allBackup => [ + /// The stores whose contents count as something the user changed. + /// + /// [lastModTime] is read off these, and sync uses that number to decide which + /// side wins — so what belongs here is what a user edits, not what the app + /// records. `connectionStats` used to be in this list and is not: connecting + /// to a server is not an edit, and every attempt was marking the device as + /// holding the newer copy of everything. + static List get _kvStores => [ setting, server, container, key, snippet, history, - connectionStats, portForward, ]; - /// Stores initialized locally. Agent conversations intentionally stay out of - /// backup and sync because they may contain terminal output and reasoning. - static List get _allStores => [..._allBackup, agentConversation]; - static Future init() async { getIt.registerLazySingleton(() => SettingStore.instance); getIt.registerLazySingleton(() => ServerStore.instance); @@ -61,22 +62,37 @@ abstract final class Stores { () => PortForwardStore.instance, ); - await Future.wait(_allStores.map((store) => store.init())); + // First and on its own. `connectionStats` and `agentConversation` create + // their tables, which means reaching the database synchronously — and a + // `Future.wait` invokes every element before awaiting any of them, so + // batching them with the stores that are still opening the file would have + // them reach a database that is still null. It did, on every cold launch. + await SqliteStore.openDatabase(); + + await Future.wait([ + ..._kvStores.map((store) => store.init()), + // Their own tables rather than rows in `kv`, so they create those. + connectionStats.init(), + agentConversation.init(), + ]); + + // Before every fixup below. Each of them writes a flag meaning "this device + // has been dealt with", and running them against the empty stores would set + // those flags over data that has not been copied across yet — so the + // records that need converting would arrive after the only pass that would + // have converted them. + await HiveImport.runIfNeeded(); await setting.removeRetiredKeys(); // Migrate sshConnectionMode from old int values to bool setting.migrateSshConnectionMode(); await setting.migrateHomeTabsAgent(); - - if (connectionStats.indexDbKeys.isEmpty) { - await connectionStats.rebuildIndexAndCompact(); - } } static int get lastModTime { var lastModTime = 0; - for (final store in _allBackup) { + for (final store in _kvStores) { final last = store.lastUpdateTs; if (last == null) { continue; diff --git a/lib/data/store/agent_conversation.dart b/lib/data/store/agent_conversation.dart index 925d000dae..f404ad2bc2 100644 --- a/lib/data/store/agent_conversation.dart +++ b/lib/data/store/agent_conversation.dart @@ -1,28 +1,34 @@ +import 'dart:async'; +import 'dart:convert'; + import 'package:fl_lib/fl_lib.dart'; -import 'package:hive_ce/hive.dart'; import 'package:meta/meta.dart'; import 'package:server_box/data/model/ai/agent_conversation.dart'; import 'package:server_box/data/model/ai/ask_ai_models.dart'; +import 'package:sqlite3/sqlite3.dart'; -class AgentConversationStore extends HiveStore { - AgentConversationStore._() - : super( - 'agent_conversation', - updateLastUpdateTsOnClear: false, - updateLastUpdateTsOnRemove: false, - updateLastUpdateTsOnSet: false, - ); +/// Agent conversations, one row each, plus which one is open per server. +/// +/// Tables rather than a K-V store because both reads are per server: "every +/// conversation for this server, newest first" was a scan of every conversation +/// in the app followed by an in-memory sort, and the 30-per-server cap was that +/// same scan again. Both are indexed queries here. +/// +/// The conversation stays one JSON column. Nothing queries inside the item list +/// — it is read whole or not at all — and the two fields that are queried are +/// lifted out beside it. +/// +/// Left out of backup and sync on purpose: these may contain terminal output +/// and reasoning. +class AgentConversationStore { + AgentConversationStore._() : _suffix = ''; + /// A distinct table name, so a test on `SqliteDb.openInMemory()` cannot + /// collide with another test's rows. @visibleForTesting - AgentConversationStore.forBox(Box testBox) - : super( - 'agent_conversation_test', - updateLastUpdateTsOnClear: false, - updateLastUpdateTsOnRemove: false, - updateLastUpdateTsOnSet: false, - ) { - box = testBox; - } + AgentConversationStore.forTest() : _suffix = '_test'; + + final String _suffix; static final instance = AgentConversationStore._(); @@ -30,22 +36,65 @@ class AgentConversationStore extends HiveStore { static const maxItemsPerConversation = 240; static const maxCharactersPerConversation = 512000; + /// Key prefixes of the Hive box these tables replaced. Only [importRow] + /// still needs them. + /// + /// TODO: delete with `HiveImport`. static const _conversationPrefix = 'conversation::'; static const _activePrefix = 'active::'; + Database get _db => SqliteDb.instance; + String get _conv => 'agent_conversation$_suffix'; + String get _active => 'agent_active$_suffix'; + + final _changes = StreamController.broadcast(); + + /// Fires after any write here. + /// + /// What `box.watch()` was: a view showing the conversation list has to notice + /// a write it did not make itself, and every write goes through this class. + Stream watch() => _changes.stream; + + Future init() async { + _db.execute( + 'CREATE TABLE IF NOT EXISTS $_conv (' + ' id TEXT NOT NULL PRIMARY KEY,' + ' server_id TEXT NOT NULL,' + ' updated_at INTEGER NOT NULL,' + ' data TEXT NOT NULL' + ') WITHOUT ROWID;', + ); + _db.execute( + 'CREATE INDEX IF NOT EXISTS idx_${_conv}_server_updated ' + 'ON $_conv(server_id, updated_at DESC);', + ); + _db.execute( + 'CREATE TABLE IF NOT EXISTS $_active (' + ' server_id TEXT NOT NULL PRIMARY KEY,' + ' conversation_id TEXT NOT NULL' + ') WITHOUT ROWID;', + ); + } + List fetchForServer(String serverId) { - final conversations = []; - for (final key in box.keys) { - if (key is! String || !key.startsWith(_conversationPrefix)) continue; - final conversation = _conversationFromValue(box.get(key)); - if (conversation?.serverId == serverId) conversations.add(conversation!); + final rows = _db.select( + 'SELECT data FROM $_conv WHERE server_id = ? ORDER BY updated_at DESC;', + [serverId], + ); + final result = []; + for (final row in rows) { + final conversation = _decode(row['data'] as String); + if (conversation != null) result.add(conversation); } - conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); - return conversations; + return result; } AgentConversation? fetch(String conversationId) { - return _conversationFromValue(box.get(_conversationKey(conversationId))); + final rows = _db.select('SELECT data FROM $_conv WHERE id = ?;', [ + conversationId, + ]); + if (rows.isEmpty) return null; + return _decode(rows.single['data'] as String); } AgentConversation? fetchActive(String serverId) { @@ -56,8 +105,13 @@ class AgentConversationStore extends HiveStore { } String? activeConversationId(String serverId) { - final value = box.get(_activeKey(serverId)); - return value is String && value.isNotEmpty ? value : null; + final rows = _db.select( + 'SELECT conversation_id FROM $_active WHERE server_id = ?;', + [serverId], + ); + if (rows.isEmpty) return null; + final value = rows.single['conversation_id'] as String; + return value.isNotEmpty ? value : null; } AgentConversation create({ @@ -89,33 +143,34 @@ class AgentConversationStore extends HiveStore { title: _normalizeTitle(conversation.title, conversation.items), items: trimItemsForStorage(conversation.items), ); - final saved = set( - _conversationKey(normalized.id), - normalized.toJson(), - updateLastUpdateTsOnSet: false, - ); - if (!saved) return false; - if (setActive) { - set( - _activeKey(normalized.serverId), - normalized.id, - updateLastUpdateTsOnSet: false, - ); + // One unit, and the caller treats `false` as "not saved". Three statements + // otherwise: a conversation could be stored but not made active, or stored + // without the over-cap ones being dropped, and the caller would be told it + // failed while part of it stood. + try { + SqliteStore.transact(() { + _upsert(normalized); + if (setActive) _setActiveRow(normalized.serverId, normalized.id); + _pruneServer(normalized.serverId); + }); + } catch (e) { + dprint('Saving AgentConversation', e); + return false; } - _pruneServer(normalized.serverId); + // After it commits, so nothing is told to re-read a state that was undone. + _changes.add(null); return true; } bool setActive(String serverId, String conversationId) { final conversation = fetch(conversationId); if (conversation == null || conversation.serverId != serverId) return false; - return set( - _activeKey(serverId), - conversationId, - updateLastUpdateTsOnSet: false, - ); + _setActiveRow(serverId, conversationId); + _changes.add(null); + return true; } + bool rename(String conversationId, String title) { final conversation = fetch(conversationId); if (conversation == null) return false; @@ -128,24 +183,27 @@ class AgentConversationStore extends HiveStore { void deleteConversation(String serverId, String conversationId) { final conversation = fetch(conversationId); if (conversation == null || conversation.serverId != serverId) return; - remove(_conversationKey(conversationId), updateLastUpdateTsOnRemove: false); - if (activeConversationId(serverId) != conversationId) return; - final remaining = fetchForServer(serverId); - if (remaining.isEmpty) { - remove(_activeKey(serverId), updateLastUpdateTsOnRemove: false); - } else { - setActive(serverId, remaining.first.id); + _db.execute('DELETE FROM $_conv WHERE id = ?;', [conversationId]); + + // Exactly once, whichever way this returns. Deleting a conversation that + // was not the active one used to return before notifying at all, leaving + // the list showing a row that is gone; promoting a replacement notified + // twice, because `setActive` notifies too. + if (activeConversationId(serverId) == conversationId) { + final remaining = fetchForServer(serverId); + if (remaining.isEmpty) { + _db.execute('DELETE FROM $_active WHERE server_id = ?;', [serverId]); + } else { + _setActiveRow(serverId, remaining.first.id); + } } + _changes.add(null); } void clearServer(String serverId) { - for (final conversation in fetchForServer(serverId)) { - remove( - _conversationKey(conversation.id), - updateLastUpdateTsOnRemove: false, - ); - } - remove(_activeKey(serverId), updateLastUpdateTsOnRemove: false); + _db.execute('DELETE FROM $_conv WHERE server_id = ?;', [serverId]); + _db.execute('DELETE FROM $_active WHERE server_id = ?;', [serverId]); + _changes.add(null); } static List trimItemsForStorage( @@ -175,14 +233,18 @@ class AgentConversationStore extends HiveStore { return List.unmodifiable(items.sublist(start)); } + /// Drops everything past the newest [maxConversationsPerServer] for a server. + /// + /// One statement, where the K-V version read and decoded every conversation + /// in the app on every save to find out which ones were past the cap. void _pruneServer(String serverId) { - final conversations = fetchForServer(serverId); - for (final conversation in conversations.skip(maxConversationsPerServer)) { - remove( - _conversationKey(conversation.id), - updateLastUpdateTsOnRemove: false, - ); - } + _db.execute( + 'DELETE FROM $_conv WHERE server_id = ? AND id NOT IN (' + ' SELECT id FROM $_conv WHERE server_id = ? ' + ' ORDER BY updated_at DESC LIMIT ?' + ');', + [serverId, serverId, maxConversationsPerServer], + ); } static int _nextUserMessage(List items, int start) { @@ -246,15 +308,59 @@ class AgentConversationStore extends HiveStore { return ''; } - static String _conversationKey(String id) => '$_conversationPrefix$id'; - static String _activeKey(String serverId) => '$_activePrefix$serverId'; + /// Takes one row out of the Hive box these tables replaced. + /// + /// Used only by `HiveImport`. The box held both kinds under one namespace, + /// told apart by the key prefix. + bool importRow(String key, Object value) { + if (key.startsWith(_activePrefix)) { + if (value is! String || value.isEmpty) return false; + _setActiveRow(key.substring(_activePrefix.length), value); + return true; + } + if (!key.startsWith(_conversationPrefix) || value is! Map) return false; + final conversation = _fromMap(Map.from(value)); + if (conversation == null) return false; + // Not through `save`: that would re-trim and re-title a record the user + // already has, and prune against a table still being filled. + _upsert(conversation); + return true; + } - static AgentConversation? _conversationFromValue(Object? value) { - if (value is! Map) return null; + void _upsert(AgentConversation conversation) { + _db.execute( + 'INSERT INTO $_conv (id, server_id, updated_at, data) VALUES (?, ?, ?, ?) ' + 'ON CONFLICT (id) DO UPDATE SET server_id = excluded.server_id, ' + 'updated_at = excluded.updated_at, data = excluded.data;', + [ + conversation.id, + conversation.serverId, + conversation.updatedAt.millisecondsSinceEpoch, + json.encode(conversation.toJson()), + ], + ); + } + + void _setActiveRow(String serverId, String conversationId) { + _db.execute( + 'INSERT INTO $_active (server_id, conversation_id) VALUES (?, ?) ' + 'ON CONFLICT (server_id) DO UPDATE SET ' + 'conversation_id = excluded.conversation_id;', + [serverId, conversationId], + ); + } + + static AgentConversation? _decode(String data) { try { - final conversation = AgentConversation.fromJson( - Map.from(value), - ); + return _fromMap(json.decode(data) as Map); + } catch (_) { + return null; + } + } + + static AgentConversation? _fromMap(Map map) { + try { + final conversation = AgentConversation.fromJson(map); if (conversation.id.isEmpty || conversation.serverId.isEmpty) return null; return conversation; } catch (_) { diff --git a/lib/data/store/cached_store.dart b/lib/data/store/cached_store.dart index cec2ac9949..711f56c49f 100644 --- a/lib/data/store/cached_store.dart +++ b/lib/data/store/cached_store.dart @@ -1,129 +1,123 @@ -import 'dart:async'; - import 'package:fl_lib/fl_lib.dart'; -abstract class CachedHiveStore extends HiveStore { - CachedHiveStore(super.boxName); +/// A [SqliteStore] whose rows are one model type, kept in a list cache. +/// +/// The cache is dropped by the write methods themselves rather than by watching +/// the store. Hive needed a watcher because a box could be written behind the +/// store's back — `Backup.restore` did exactly that — and a watcher then had to +/// be suppressed around the store's own writes so they did not each cost a +/// reload. Nothing can reach the database except through here now, so +/// invalidating in [set], [remove] and [clear] covers every path. +abstract class CachedSqliteStore extends SqliteStore { + CachedSqliteStore(super.name); List? _cache; - StreamSubscription? _boxWatchSub; - bool _suppressWatch = false; List? get cachedItems => _cache; + /// The row key for [item]. String getKey(T item); + /// Rebuilds one item from its stored JSON. + T? fromJson(Map json); + @override - Future init() async { - await super.init(); - _boxWatchSub?.cancel(); - _boxWatchSub = box.watch().listen((_) { - if (!_suppressWatch) { - _cache = null; - } - }); + bool set( + String key, + V val, { + StoreToObj? toObj, + bool? updateLastUpdateTsOnSet, + }) { + final res = super.set( + key, + val, + toObj: toObj, + updateLastUpdateTsOnSet: updateLastUpdateTsOnSet, + ); + if (res) _cache = null; + return res; } @override - bool clear({bool? updateLastUpdateTsOnClear}) { - _suppressWatch = true; - try { - _cache = null; - return super.clear(updateLastUpdateTsOnClear: updateLastUpdateTsOnClear); - } finally { - _suppressWatch = false; - } + bool remove(String key, {bool? updateLastUpdateTsOnRemove}) { + final res = super.remove( + key, + updateLastUpdateTsOnRemove: updateLastUpdateTsOnRemove, + ); + if (res) _cache = null; + return res; } - void invalidateCache() { + @override + bool clear({bool? updateLastUpdateTsOnClear}) { _cache = null; + return super.clear(updateLastUpdateTsOnClear: updateLastUpdateTsOnClear); } - void put(T item) { - _suppressWatch = true; - try { - set(getKey(item), item); - _cache = null; - } finally { - _suppressWatch = false; - } - } + void invalidateCache() => _cache = null; - void putRaw(T item) { - _suppressWatch = true; - try { - box.put(getKey(item), item); - _cache = null; - } finally { - _suppressWatch = false; - } - } + void put(T item) => set(getKey(item), item); - List fetch() { - return List.from(_cache ??= _loadAll()); - } + /// A copy, so a caller sorting or filtering the result cannot reorder the + /// cache everyone else reads. + List fetch() => List.from(_cache ??= _loadAll()); + /// One query, not one per key. + /// + /// Under Hive `box.get` was a map lookup, so reading each key in turn was + /// free; each is a prepared-statement round trip now, and this runs on every + /// cache miss — which is every write, since the write methods drop the cache. List _loadAll() { final result = []; - for (final key in keys()) { - final item = _getAndConvert(key); - if (item != null) { - result.add(item); - } - } - return result; - } - - T? _getAndConvert(String key) { - final val = get(key); - if (val != null) return val; - - final raw = box.get(key); - if (raw == null) return null; - - if (raw is Map) { + for (final entry in getAllMap().entries) { + final raw = entry.value; + if (raw is! Map) continue; try { final item = fromJson(Map.from(raw)); - if (item != null) { - putRaw(item); - } - return item; + if (item != null) result.add(item); } catch (e) { dprint('Parsing $T from JSON', e); } } - return null; + return result; } - T? fromJson(Map json); - - void deleteById(String id) { - _suppressWatch = true; + /// Reads one row, bypassing the cache. + T? fetchOneRaw(String key) { + final raw = get(key); + if (raw is! Map) return null; try { - remove(id); - _cache = null; - } finally { - _suppressWatch = false; + return fromJson(Map.from(raw)); + } catch (e) { + dprint('Parsing $T from JSON', e); + return null; } } - void delete(T item) { - deleteById(getKey(item)); - } + void deleteById(String id) => remove(id); + + void delete(T item) => deleteById(getKey(item)); void update(T old, T newItem) { + final oldKey = getKey(old); if (!have(old)) { throw Exception('Old $T: $old not found'); } - _suppressWatch = true; - try { - remove(getKey(old)); - set(getKey(newItem), newItem); - _cache = null; - } finally { - _suppressWatch = false; + final newKey = getKey(newItem); + if (oldKey == newKey) { + // In place, which is what every caller but the id migrations does. An + // upsert is one statement and needs no transaction around it. + set(newKey, newItem); + return; } + // The key moved, so this is a delete and an insert. As one unit: a crash + // between them would leave the record under neither key. `transact` nests, + // so this is also safe if a caller has already opened one. + SqliteStore.transact(() { + remove(oldKey); + set(newKey, newItem); + }); } - bool have(T item) => get(getKey(item)) != null; + bool have(T item) => get(getKey(item)) != null; } diff --git a/lib/data/store/connection_stats.dart b/lib/data/store/connection_stats.dart index 3b33aed0ae..d3eec8f55f 100644 --- a/lib/data/store/connection_stats.dart +++ b/lib/data/store/connection_stats.dart @@ -1,135 +1,74 @@ -import 'dart:io'; - import 'package:fl_lib/fl_lib.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/model/server/connection_stat.dart'; - -class ConnectionStatsStore extends HiveStore { - ConnectionStatsStore._() : super('connection_stats'); +import 'package:sqlite3/sqlite3.dart'; + +/// Connection attempts, one row each. +/// +/// A table rather than a K-V store because every question asked of it is a +/// range over one server's history, and answering those out of a K-V store took +/// a second store holding per-server lists of keys, plus the code to keep those +/// lists in step: rebuild, update-on-insert, prune-to-100 and expire-after-30- +/// days were four hand-written passes over the records. They are two `DELETE` +/// statements and an index here. +/// +/// That second store was also the one thing in the app that was never +/// encrypted, because it was a Hive box opened without a cipher. Being a table +/// in the shared database, this is behind the same key as everything else. +class ConnectionStatsStore { + ConnectionStatsStore._(); static final instance = ConnectionStatsStore._(); - static const _indexBoxName = 'conn_stats_index'; + /// Kept per server, oldest dropped first. static const _maxRecordsPerServer = 100; - /// Not `final`, for the same reason [box] is not: [init] can run twice. - late Box _indexBox; - - @override - Future init() async { - await super.init(); - _indexBox = await Hive.openBox( - _indexBoxName, - path: box.path?.substring(0, box.path!.lastIndexOf(Pfs.seperator)), - ); - } - - Future rebuildIndexAndCompact() async { - await _cleanAllOldAndRebuildIndex(); - await _compactIfNeeded(); - } - - Future _rebuildIndexCore() async { - final cutoffTime = DateTime.now().subtract(const Duration(days: 30)); - final serverIdToKeys = >{}; - - for (final key in keys().toList()) { - final stat = get(key); - if (stat == null) continue; - - if (stat.timestamp.isBefore(cutoffTime)) { - remove(key); - continue; - } - - final serverId = stat.serverId; - serverIdToKeys.putIfAbsent(serverId, () => []).add(key); - } - - final idxKeysToDelete = _indexBox.keys - .where((k) => k.toString().startsWith('idx_')) - .toList(); - for (final k in idxKeysToDelete) { - await _indexBox.delete(k); - } - - for (final entry in serverIdToKeys.entries) { - final keys = entry.value; - if (keys.length > _maxRecordsPerServer) { - final keyStatPairs = <(String, ConnectionStat)>[]; - for (final key in keys) { - final stat = get(key); - if (stat != null) keyStatPairs.add((key, stat)); - } - keyStatPairs.sort((a, b) => b.$2.timestamp.compareTo(a.$2.timestamp)); - final toKeep = keyStatPairs - .take(_maxRecordsPerServer) - .map((p) => p.$1) - .toList() - .reversed - .toList(); - final toRemove = keyStatPairs.skip(_maxRecordsPerServer); - for (final pair in toRemove) { - remove(pair.$1); - } - await _indexBox.put('idx_${entry.key}', toKeep); - } else { - await _indexBox.put('idx_${entry.key}', keys); - } - } - } + /// Dropped regardless of the per-server count. + static const _retention = Duration(days: 30); - Future _cleanAllOldAndRebuildIndex() async { - await _rebuildIndexCore(); - } + /// How many recent attempts a summary carries. The list card shows three of + /// them and the detail dialog shows the rest, both off the same object. + static const _recentPerServer = 20; - Future _compactIfNeeded() async { - try { - await box.compact(); - await _indexBox.compact(); - } catch (e, st) { - Loggers.app.warning('Auto compact failed during init', e, st); - } - } + Database get _db => SqliteDb.instance; - Future _updateIndex(String serverId, String recordKey) async { - final indexKey = 'idx_$serverId'; - final keys = - (_indexBox.get(indexKey) as List?)?.cast().toList() ?? []; + Future init() async { + _db.execute(''' +CREATE TABLE IF NOT EXISTS conn_stat ( + id TEXT NOT NULL PRIMARY KEY, + server_id TEXT NOT NULL, + server_name TEXT NOT NULL, + timestamp INTEGER NOT NULL, + result TEXT NOT NULL, + error_message TEXT NOT NULL DEFAULT '', + duration_ms INTEGER NOT NULL +) WITHOUT ROWID; +'''); + // Every read is "this server, newest first", and the per-server cap below + // is the same order with a LIMIT. + _db.execute( + 'CREATE INDEX IF NOT EXISTS idx_conn_stat_server_ts ' + 'ON conn_stat(server_id, timestamp DESC);', + ); + // The age sweep asks about `timestamp` alone, which the index above cannot + // serve — its leading column is `server_id`. + _db.execute( + 'CREATE INDEX IF NOT EXISTS idx_conn_stat_ts ON conn_stat(timestamp);', + ); - if (!keys.contains(recordKey)) { - keys.add(recordKey); - if (keys.length > _maxRecordsPerServer) { - await _pruneExcessRecords(serverId, keys); - } - await _indexBox.put(indexKey, keys); - } + _expire(); } - Future _pruneExcessRecords(String serverId, List keys) async { - if (keys.length <= _maxRecordsPerServer) return; - - final keyStatPairs = <(String, ConnectionStat)>[]; - for (final key in keys) { - final stat = get(key); - if (stat != null) { - keyStatPairs.add((key, stat)); - } - } - - keyStatPairs.sort((a, b) => b.$2.timestamp.compareTo(a.$2.timestamp)); - - final toRemove = keyStatPairs.skip(_maxRecordsPerServer); - for (final pair in toRemove) { - remove(pair.$1); - keys.remove(pair.$1); - } + Future recordConnection(ConnectionStat stat) async { + _insert(_idOf(stat), stat); + _prune(stat.serverId); } - Future recordConnection(ConnectionStat stat) async { - final key = '${stat.serverId}_${stat.timestamp.millisecondsSinceEpoch}'; - set(key, stat); - await _updateIndex(stat.serverId, key); + List getConnectionHistory(String serverId) { + final rows = _db.select( + 'SELECT * FROM conn_stat WHERE server_id = ? ORDER BY timestamp DESC;', + [serverId], + ); + return rows.map(_fromRow).toList(); } ServerConnectionStats getServerStats(String serverId, String serverName) { @@ -147,133 +86,204 @@ class ConnectionStatsStore extends HiveStore { ); } - final totalAttempts = allStats.length; var successCount = 0; DateTime? lastSuccessTime; DateTime? lastFailureTime; final recentConnections = []; for (final stat in allStats) { - final isSuccess = stat.result.isSuccess; - if (isSuccess) { + if (stat.result.isSuccess) { successCount += 1; lastSuccessTime ??= stat.timestamp; } else { lastFailureTime ??= stat.timestamp; } - if (recentConnections.length < 20) { + if (recentConnections.length < _recentPerServer) { recentConnections.add(stat); } } - final failureCount = totalAttempts - successCount; - final successRate = totalAttempts > 0 - ? (successCount / totalAttempts) - : 0.0; - + final totalAttempts = allStats.length; return ServerConnectionStats( serverId: serverId, serverName: serverName, totalAttempts: totalAttempts, successCount: successCount, - failureCount: failureCount, + failureCount: totalAttempts - successCount, lastSuccessTime: lastSuccessTime, lastFailureTime: lastFailureTime, recentConnections: recentConnections, - successRate: successRate, + successRate: successCount / totalAttempts, ); } - List getConnectionHistory(String serverId) { - final indexKey = 'idx_$serverId'; - final keys = (_indexBox.get(indexKey) as List?)?.cast() ?? []; - - final stats = []; - for (final key in keys.reversed) { - final stat = get(key); - if (stat != null) { - stats.add(stat); - } - } - return stats; - } - + /// Every server's summary, in two queries. + /// + /// One `GROUP BY` to enumerate servers and then a full history read per + /// server meant 21 queries and up to 2000 decoded records to draw 20 summary + /// cards. The counters are an aggregate, and the only rows that reach the UI + /// are the newest [_recentPerServer] per server — which a window function + /// bounds in the database rather than after decoding everything. List getAllServerStats() { - final indexKeys = _indexBox.keys - .where((k) => k is String && k.startsWith('idx_')) - .cast() - .toList(); - - final allStats = []; - for (final indexKey in indexKeys) { - final serverId = indexKey.substring(4); - final keys = (_indexBox.get(indexKey) as List?)?.cast() ?? []; - - if (keys.isEmpty) continue; - - String? serverName; - for (final key in keys.reversed) { - final stat = get(key); - if (stat != null) { - serverName = stat.serverName; - break; - } - } - - if (serverName == null) continue; + const success = 'success'; + assert(ConnectionResult.success.name == success); + + final totals = _db.select( + 'SELECT server_id,' + ' COUNT(*) AS total,' + " SUM(CASE WHEN result = '$success' THEN 1 ELSE 0 END) AS successes," + " MAX(CASE WHEN result = '$success' THEN timestamp END) AS last_ok," + " MAX(CASE WHEN result <> '$success' THEN timestamp END) AS last_bad " + 'FROM conn_stat GROUP BY server_id;', + ); + if (totals.isEmpty) return const []; + + // Newest first within each server, so the first row of a group is also + // where the current name comes from — a server can be renamed, and the + // older rows keep whatever it was called at the time. + final recentRows = _db.select( + 'SELECT * FROM (' + ' SELECT *, ROW_NUMBER() OVER (' + ' PARTITION BY server_id ORDER BY timestamp DESC' + ' ) AS rn FROM conn_stat' + ') WHERE rn <= ? ORDER BY server_id, timestamp DESC;', + [_recentPerServer], + ); - final stats = getServerStats(serverId, serverName); - allStats.add(stats); + final recent = >{}; + for (final row in recentRows) { + (recent[row['server_id'] as String] ??= []).add(_fromRow(row)); } - return allStats; + final result = []; + for (final row in totals) { + final serverId = row['server_id'] as String; + final recentConnections = recent[serverId] ?? const []; + if (recentConnections.isEmpty) continue; + + final total = row['total'] as int; + final successCount = (row['successes'] as num?)?.toInt() ?? 0; + result.add( + ServerConnectionStats( + serverId: serverId, + serverName: recentConnections.first.serverName, + totalAttempts: total, + successCount: successCount, + failureCount: total - successCount, + lastSuccessTime: _timeOf(row['last_ok']), + lastFailureTime: _timeOf(row['last_bad']), + recentConnections: recentConnections, + successRate: total > 0 ? successCount / total : 0.0, + ), + ); + } + return result; } + static DateTime? _timeOf(Object? millis) => millis is int + ? DateTime.fromMillisecondsSinceEpoch(millis) + : null; + Future clearAll() async { - await box.clear(); - await _indexBox.clear(); + _db.execute('DELETE FROM conn_stat;'); } Future clearServerStats(String serverId) async { - final indexKey = 'idx_$serverId'; - final keys = (_indexBox.get(indexKey) as List?)?.cast() ?? []; + _db.execute('DELETE FROM conn_stat WHERE server_id = ?;', [serverId]); + } - for (final key in keys) { - remove(key); - } - await _indexBox.delete(indexKey); + /// Drops everything past the per-server cap. + /// + /// Served by `idx_conn_stat_server_ts`, and bounded by one server's rows. + /// Cheap enough to run on every insert, which is how the cap stays exact. + void _prune(String serverId) { + _db.execute( + 'DELETE FROM conn_stat WHERE server_id = ? AND id NOT IN (' + ' SELECT id FROM conn_stat WHERE server_id = ? ' + ' ORDER BY timestamp DESC LIMIT ?' + ');', + [serverId, serverId, _maxRecordsPerServer], + ); + } + + /// Drops everything past the age bound. + /// + /// At [init], not on every insert. Recording a connection happens on every + /// attempt against every server — the status page refreshes on a timer — and + /// the common case is that nothing has expired, so paying for it each time + /// bought nothing. The bound is about not keeping a month-old record for + /// ever, which a sweep per launch satisfies. + void _expire() { + _db.execute('DELETE FROM conn_stat WHERE timestamp < ?;', [ + DateTime.now().subtract(_retention).millisecondsSinceEpoch, + ]); } Future compact() async { - Loggers.app.info('Start compacting connection_stats database...'); + Loggers.app.info('Start compacting the store database...'); try { - await box.compact(); - await _indexBox.compact(); - Loggers.app.info('Finished compacting connection_stats database'); + SqliteDb.vacuum(); + Loggers.app.info('Finished compacting the store database'); } catch (e, st) { - Loggers.app.warning('Failed compacting connection_stats database', e, st); + Loggers.app.warning('Failed compacting the store database', e, st); rethrow; } } - String? get dbPath => box.path; - - String? get indexDbPath => _indexBox.path; - - Iterable get indexDbKeys => - _indexBox.keys.where((k) => k.toString().startsWith('idx_')); - - Future dbSizeAsync() async { - final path = dbPath; - if (path == null) return 0; - final file = File(path); - return await file.exists() ? await file.length() : 0; + /// Size of the whole store database, not of this table. + /// + /// Every store shares one file, so there is no per-table number to report and + /// the compaction this feeds is `VACUUM` on that file. + Future dbSizeAsync() => SqliteDb.size(); + + /// Takes one row out of the Hive box this table replaced. + /// + /// Used only by `HiveImport`. It writes the record under the key it had, so + /// re-running the import overwrites rather than duplicates. + bool importRow(String key, Object value) { + if (value is! Map) return false; + try { + _insert(key, ConnectionStat.fromJson(Map.from(value))); + return true; + } catch (e) { + dprint('Importing ConnectionStat', e); + return false; + } } - Future indexDbSizeAsync() async { - final path = indexDbPath; - if (path == null) return 0; - final file = File(path); - return await file.exists() ? await file.length() : 0; + void _insert(String id, ConnectionStat stat) { + _db.execute( + 'INSERT INTO conn_stat ' + '(id, server_id, server_name, timestamp, result, error_message, duration_ms) ' + 'VALUES (?, ?, ?, ?, ?, ?, ?) ' + 'ON CONFLICT (id) DO UPDATE SET ' + 'server_name = excluded.server_name, timestamp = excluded.timestamp, ' + 'result = excluded.result, error_message = excluded.error_message, ' + 'duration_ms = excluded.duration_ms;', + [ + id, + stat.serverId, + stat.serverName, + stat.timestamp.millisecondsSinceEpoch, + stat.result.name, + stat.errorMessage, + stat.durationMs, + ], + ); } + + static String _idOf(ConnectionStat stat) => + '${stat.serverId}_${stat.timestamp.millisecondsSinceEpoch}'; + + static ConnectionStat _fromRow(Row row) => ConnectionStat( + serverId: row['server_id'] as String, + serverName: row['server_name'] as String, + timestamp: DateTime.fromMillisecondsSinceEpoch(row['timestamp'] as int), + result: ConnectionResult.values.firstWhere( + (e) => e.name == row['result'], + orElse: () => ConnectionResult.unknownError, + ), + errorMessage: row['error_message'] as String? ?? '', + durationMs: row['duration_ms'] as int, + ); } diff --git a/lib/data/store/container.dart b/lib/data/store/container.dart index d5b456584e..b00f23214e 100644 --- a/lib/data/store/container.dart +++ b/lib/data/store/container.dart @@ -5,17 +5,17 @@ import 'package:server_box/data/res/store.dart'; const _keyConfig = 'providerConfig'; const _keyHost = 'containerHost'; -class ContainerStore extends HiveStore { +class ContainerStore extends SqliteStore { ContainerStore._() : super('docker'); static final instance = ContainerStore._(); String? fetch(String? id, ContainerType type) { - final host = box.get(_hostKey(id, type)); + final host = get(_hostKey(id, type)); if (host != null || type == ContainerType.podman) return host; // Preserve existing Docker host settings stored before per-runtime hosts. - return box.get(id); + return id == null ? null : get(id); } void put(String id, ContainerType type, String host) { @@ -31,7 +31,7 @@ class ContainerStore extends HiveStore { '$_keyHost${type.name}${id ?? ''}'; ContainerType getType([String id = '']) { - final cfg = box.get(_keyConfig + id); + final cfg = get(_keyConfig + id); if (cfg != null) { final type = ContainerType.values.firstWhereOrNull( (e) => e.toString() == cfg, diff --git a/lib/data/store/history.dart b/lib/data/store/history.dart index 5c4f85eca5..eb70a91a3b 100644 --- a/lib/data/store/history.dart +++ b/lib/data/store/history.dart @@ -1,71 +1,72 @@ import 'package:fl_lib/fl_lib.dart'; -import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:meta/meta.dart'; /// index from 0 -> n : latest -> oldest class _ListHistory { - final List _history; + const _ListHistory({required SqliteStore store, required String name}) + : _store = store, + _name = name; + + final SqliteStore _store; final String _name; - final Box _box; - _ListHistory({required Box box, required String name}) - : _box = box, - _name = name, - _history = box.get(name, defaultValue: [])!; + /// Read through on every call rather than cached at construction. + /// + /// The Hive version held the list it was built with and wrote the same + /// instance back, so it was the store's contents only as long as nothing else + /// touched the key. Nothing did, but a restore now goes through the store + /// like everything else, and a snapshot taken at first access would outlive + /// it. + List get all => + _store.get(_name)?.cast().toList() ?? []; void add(String path) { - _history.remove(path); - _history.insert(0, path); - _box.put(_name, _history); + final history = all..remove(path); + history.insert(0, path); + _store.set(_name, history); } - List get all => _history; - - void clear() { - _history.clear(); - _box.put(_name, _history); - } + void clear() => _store.set(_name, const []); } class _MapHistory { - final Map _history; + const _MapHistory({required SqliteStore store, required String name}) + : _store = store, + _name = name; + + final SqliteStore _store; final String _name; - final Box _box; - _MapHistory({required Box box, required String name}) - : _box = box, - _name = name, - _history = box.get(name, defaultValue: {})!; + Map get _all => + _store.get(_name)?.cast() ?? {}; void put(String id, String val) { - _history[id] = val; - _box.put(_name, _history); + _store.set(_name, {..._all, id: val}); } - String? fetch(String id) => _history[id]; + String? fetch(String id) => _all[id]; } -class HistoryStore extends HiveStore { +class HistoryStore extends SqliteStore { HistoryStore._() : super('history'); - /// The same seam [SettingStore.forBox] and [ServerStore.forBox] have. + /// The same seam [ServerStore.forTest] has: a distinct store name, so a test + /// on `SqliteDb.openInMemory()` cannot collide with another test's rows. /// /// This one holds the terminal tab set, which is the piece of session state /// that does survive a relaunch — Flutter's own restoration does not, here — /// so it is what a test of "what comes back" has to be able to write. @visibleForTesting - HistoryStore.forBox(Box testBox) : super('history_test') { - box = testBox; - } + HistoryStore.forTest() : super('history_test'); static final instance = HistoryStore._(); - late final sftpGoPath = _ListHistory(box: box, name: 'sftpPath'); + late final sftpGoPath = _ListHistory(store: this, name: 'sftpPath'); - late final sftpLastPath = _MapHistory(box: box, name: 'sftpLastPath'); + late final sftpLastPath = _MapHistory(store: this, name: 'sftpLastPath'); late final sshServerHistory = _ListHistory( - box: box, + store: this, name: 'sshServerHistory', ); diff --git a/lib/data/store/migrations/m002_nest_ssh.dart b/lib/data/store/migrations/m002_nest_ssh.dart deleted file mode 100644 index 06fe1a1616..0000000000 --- a/lib/data/store/migrations/m002_nest_ssh.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:fl_lib/fl_lib.dart'; -import 'package:server_box/data/model/server/server_private_info.dart'; -import 'package:server_box/data/store/schema.dart'; -import 'package:server_box/data/store/server.dart'; -import 'package:server_box/hive/spi_legacy_adapter.dart'; - -/// v2 -> v3: rewrites every stored server so its SSH fields live under -/// `Spi.ssh` instead of flat on the record. -/// -/// The read half is [SpiLegacyAdapter], registered on the old typeId; this -/// step only has to put each record back, which the generated adapter then -/// writes under the current typeId in the current shape. -/// -/// Safe to re-run: a record already in the new shape is decoded by the current -/// adapter and written back unchanged. The version is only recorded once the -/// whole pass completes, so a crash part-way means the next launch repeats it. -class SpiNestSshMigration implements SchemaMigration { - const SpiNestSshMigration(); - - @override - int get from => 2; - - @override - Future apply() async { - final store = ServerStore.instance; - final migrated = {}; - - for (final key in store.box.keys) { - if (key is! String) continue; - try { - // Untyped: a v2 record decodes to LegacySpiV2, a v3 one to Spi - final raw = store.box.get(key); - final spi = switch (raw) { - final LegacySpiV2 legacy => legacy.toSpi(), - final Spi spi => spi, - _ => null, - }; - if (spi != null) migrated[key] = spi; - } catch (e, s) { - // One unreadable record must not block the rest: leaving the whole - // store on v2 would mean every launch retries and fails the same way - Loggers.app.warning('Skipping unreadable server record "$key"', e, s); - } - } - - for (final entry in migrated.entries) { - // Keyed by the original key, not `spi.id`: records written before 1155 - // are stored under `user@ip:port`, and `ServerStore.migrateIds` rekeys - // them afterwards - store.set(entry.key, entry.value); - } - - Loggers.app.info('Nested SSH credentials for ${migrated.length} servers'); - } -} diff --git a/lib/data/store/migrations/m003_hive_to_sqlite.dart b/lib/data/store/migrations/m003_hive_to_sqlite.dart new file mode 100644 index 0000000000..c4859a4f13 --- /dev/null +++ b/lib/data/store/migrations/m003_hive_to_sqlite.dart @@ -0,0 +1,243 @@ +import 'dart:io'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:server_box/data/res/store.dart'; +import 'package:server_box/data/store/schema.dart'; +import 'package:server_box/hive/spi_legacy_adapter.dart'; + +/// Copies every Hive box into the SQLite stores, once per device. +/// +/// Not a [SchemaMigration]: those run against one storage engine and are keyed +/// on a version number that itself lives in a store. This has to happen before +/// that number can be read at all, because on the launch that upgrades an +/// install the SQLite side is empty and would report a fresh install's default. +/// +/// TODO: delete this, `lib/hive/`, the `hive_ce*` dependencies and the +/// `Hive.initFlutter()` call in `main.dart` once no supported install can still +/// be on Hive. Keep [SpiLegacyAdapter] until then — reading a pre-v3 record is +/// what [_toSpi] needs it for. +abstract final class HiveImport { + /// Internal, so it stays out of backups and out of `lastUpdateTs`. + static const _markerKey = '${StoreDefaults.prefixKey}hiveImported'; + + /// Which boxes have been copied, for an import finishing across launches. + /// + /// Removed once [_markerKey] is written, since it answers nothing after that. + static const _doneKey = '${StoreDefaults.prefixKey}hiveImportedBoxes'; + + /// Box name -> what takes one of its rows. + /// + /// Most go to a K-V store under the same key. The last two own tables now, so + /// they take the row apart themselves. + /// + /// `conn_stats_index` is deliberately absent: it held nothing that is not + /// derivable from the records, and it is the one box that was never + /// encrypted — so it is deleted below rather than carried across. + static Map get _boxes => { + 'setting': _intoKv(Stores.setting), + 'server': _intoKv(Stores.server), + 'docker': _intoKv(Stores.container), + 'key': _intoKv(Stores.key), + 'snippet': _intoKv(Stores.snippet), + 'history': _intoKv(Stores.history), + 'port_forward': _intoKv(Stores.portForward), + 'connection_stats': Stores.connectionStats.importRow, + 'agent_conversation': Stores.agentConversation.importRow, + }; + + /// `updateLastUpdateTsOnSet: false`: the timestamps are copied across with + /// everything else, and stamping each row as it lands would overwrite them + /// with "now" and tell the next sync that this device holds the newer copy of + /// data it has just finished reading off its own disk. + static bool Function(String, Object) _intoKv(SqliteStore store) => + (key, value) => store.set(key, value, updateLastUpdateTsOnSet: false); + + /// Runs the import if this device has data in Hive and none in SQLite yet. + /// + /// Safe to re-run: nothing is deleted from Hive, so a crash part-way through + /// leaves the marker unwritten and the next launch copies what it had not + /// got to yet. + /// + /// Progress is per box rather than all-or-nothing, because neither end of + /// that choice is safe. Marking the import done when only some boxes opened + /// drops the rest for good — the marker is the first thing checked, so no + /// later launch retries them. Leaving the marker unwritten instead re-copies + /// the boxes that *did* open, and the app is usable in the meantime, so that + /// overwrites whatever the user changed between the two launches. Recording + /// which boxes landed avoids both: a box is copied once, and one that could + /// not be read is retried until it can. + static Future runIfNeeded() async { + if (Stores.setting.get(_markerKey) == true) return; + + // The same directory `HiveStore.init` opens from, asked of it rather than + // recomputed: the macOS sandbox and the mobile platforms each answer this + // differently, and looking in the wrong one reads as "fresh install". + final dir = await HiveStore.boxDir; + // Both names. `HiveStore.init` opens `_enc` and *then* folds an + // existing plain `.hive` into it, so an install old enough to predate + // box encryption has only the plain files — and looking for `_enc` alone + // read that device as a fresh install and dropped everything it had. + // `_importBox` goes through `HiveStore`, so it handles either. + final present = _boxes.keys + .where( + (name) => + File(dir.joinPath('${name}_enc.hive')).existsSync() || + File(dir.joinPath('$name.hive')).existsSync(), + ) + .toList(); + if (present.isEmpty) { + // A fresh install. Record the current layout so the migrator does not + // walk it through steps written for data it never had. + SchemaVersion.initFresh(); + Stores.setting.set(_markerKey, true); + return; + } + + final done = _doneBoxes(); + final pending = present.where((name) => !done.contains(name)).toList(); + Loggers.app.info( + 'Importing ${pending.length} Hive boxes into SQLite, ' + '${done.length} already copied', + ); + + var copied = 0; + for (final name in pending) { + final result = await _importBox(name, _boxes[name]!); + copied += result.copied; + if (result.opened) done.add(name); + } + + final unread = present.where((name) => !done.contains(name)).toList(); + if (unread.isNotEmpty) { + // A box fails to open when the keychain is briefly unavailable — the + // device still locked at launch, on iOS — and an install old enough to + // predate box encryption has some boxes that need it and some that do + // not, so this is reached with part of the data across and part not. + _setDoneBoxes(done); + Loggers.app.warning( + 'Imported $copied rows from Hive; $unread unread, ' + 'left to the next launch', + ); + // What did land is already in the current shape, so the version is set + // now: a launch in this state runs the migrator like any other, and the + // step for a shape this data no longer has must not be applied to it. + if (done.isNotEmpty) SchemaVersion.initFresh(); + return; + } + + Loggers.app.info('Imported $copied rows from Hive, every box read'); + _dropPlaintextIndex(dir); + + // The copy nests a pre-v3 server record on the way across, which is what + // the v2 -> v3 step used to do in place, so what has landed is current by + // construction. + SchemaVersion.initFresh(); + Stores.setting.set(_markerKey, true); + Stores.setting.remove(_doneKey); + } + + static Set _doneBoxes() { + final raw = Stores.setting.get(_doneKey); + if (raw == null) return {}; + return raw.whereType().toSet(); + } + + static void _setDoneBoxes(Set names) => + Stores.setting.set(_doneKey, names.toList()); + + static Future<({bool opened, int copied})> _importBox( + String name, + bool Function(String, Object) into, + ) async { + final legacy = HiveStore(name); + try { + await legacy.init(); + } catch (e, s) { + // One box that will not open must not stop the others: the alternative is + // an install that keeps all of its data and can reach none of it. + Loggers.app.warning('Hive box "$name" did not open; skipped', e, s); + return (opened: false, copied: 0); + } + + var copied = 0; + try { + // One transaction per box. A connection-stats box can hold thousands of + // rows, and a commit each would be thousands of durability barriers. + SqliteStore.transact(() { + for (final key in legacy.box.keys) { + if (key is! String) continue; + // Per record, because reading one goes through a `TypeAdapter`: a value + // written under a typeId this build no longer registers, or a truncated + // one, throws. Letting that escape would fail the launch — and fail it + // again on every launch after, since the marker stays unwritten. The + // v2 -> v3 migration this replaced caught per record for the same + // reason. + try { + final raw = legacy.box.get(key); + if (raw == null) continue; + + final value = _toSpi(raw) ?? raw; + final ok = into(key, _jsonSafe(value as Object)); + if (ok) { + copied++; + } else { + Loggers.app.warning( + 'Could not import "$name/$key" (${value.runtimeType})', + ); + } + } catch (e, s) { + Loggers.app.warning('Skipping unreadable record "$name/$key"', e, s); + } + } + }); + } finally { + await legacy.box.close(); + } + return (opened: true, copied: copied); + } + + /// A pre-v3 server record, nested into the current shape. + /// + /// Returns null for anything else, including a record already in the current + /// shape — those encode themselves. + static Object? _toSpi(Object raw) => + raw is LegacySpiV2 ? raw.toSpi() : null; + + /// The value as something made of maps, lists and primitives. + /// + /// A Hive box hands back whatever its adapter decoded — a `ConnectionStat`, + /// not a map. The K-V stores would encode that on write, but the two + /// table-backed stores parse what they are given with `fromJson`, so both + /// kinds of destination are handed the same shape. + static Object _jsonSafe(Object value) { + if (value is Map || value is List || value is Enum) return value; + if (value is num || value is String || value is bool) return value; + try { + final json = (value as dynamic).toJson(); + if (json is Object) return json; + } catch (e) { + // Usually just "no `toJson`", but not always — a model whose `toJson` + // throws looks the same from here, and the destination's own warning + // only names the type. + Loggers.app.warning('No JSON form for ${value.runtimeType}', e); + } + return value; + } + + /// Removes the one box that was never encrypted. + /// + /// The other files are kept so a bad import can be rolled back to, but this + /// one holds no data that is not derivable from the records, and leaving it + /// would leave `_` for every connection sitting in + /// plaintext beside a database that exists to not do that. + static void _dropPlaintextIndex(String dir) { + for (final suffix in const ['.hive', '.lock']) { + final file = File(dir.joinPath('conn_stats_index$suffix')); + try { + if (file.existsSync()) file.deleteSync(); + } catch (e, s) { + Loggers.app.warning('Could not delete ${file.path}', e, s); + } + } + } +} diff --git a/lib/data/store/port_forward.dart b/lib/data/store/port_forward.dart index 0754260237..475a349887 100644 --- a/lib/data/store/port_forward.dart +++ b/lib/data/store/port_forward.dart @@ -1,7 +1,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/data/model/server/port_forward.dart'; -class PortForwardStore extends HiveStore { +class PortForwardStore extends SqliteStore { PortForwardStore._() : super('port_forward'); static final instance = PortForwardStore._(); @@ -12,30 +12,18 @@ class PortForwardStore extends HiveStore { List fetch(String serverId) { final configs = []; - for (final key in keys()) { - final config = get( - key, - fromObj: (val) { - if (val is PortForwardConfig) return val; - if (val is Map) { - final map = val.toStrDynMap; - if (map == null) return null; - try { - final config = PortForwardConfig.fromJson( - map as Map, - ); - put(config); - return config; - } catch (e) { - dprint('Parsing PortForwardConfig from JSON', e); - } - } - return null; - }, - ); - if (config != null && config.serverId == serverId) { - configs.add(config); + // One query, not one per key: `getAllMap` reads the store in a single + // statement. + for (final raw in getAllMap().values) { + if (raw is! Map) continue; + final PortForwardConfig config; + try { + config = PortForwardConfig.fromJson(Map.from(raw)); + } catch (e) { + dprint('Parsing PortForwardConfig from JSON', e); + continue; } + if (config.serverId == serverId) configs.add(config); } return configs; } diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart index c2fbf38a27..2e6bef399a 100644 --- a/lib/data/store/private_key.dart +++ b/lib/data/store/private_key.dart @@ -1,19 +1,15 @@ -import 'package:fl_lib/fl_lib.dart'; -import 'package:hive_ce/hive.dart'; import 'package:meta/meta.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/store/cached_store.dart'; import 'package:server_box/data/store/server.dart'; -class PrivateKeyStore extends CachedHiveStore { +class PrivateKeyStore extends CachedSqliteStore { PrivateKeyStore._() : super('key'); - /// See [ServerStore.forBox]. + /// See [ServerStore.forTest]. @visibleForTesting - PrivateKeyStore.forBox(Box testBox) : super('key_test') { - box = testBox; - } + PrivateKeyStore.forTest() : super('key_test'); static final instance = PrivateKeyStore._(); @@ -31,20 +27,6 @@ class PrivateKeyStore extends CachedHiveStore { if (pki.id == id) return pki; } } - return _decode(box.get(id)); - } - - PrivateKeyInfo? _decode(dynamic val) { - if (val is PrivateKeyInfo) return val; - if (val is Map) { - final map = val.toStrDynMap; - if (map == null) return null; - try { - return PrivateKeyInfo.fromJson(map as Map); - } catch (e) { - dprint('Parsing PrivateKeyInfo from JSON', e); - } - } - return null; + return fetchOneRaw(id); } } diff --git a/lib/data/store/schema.dart b/lib/data/store/schema.dart index 52ea5237ef..6475814faf 100644 --- a/lib/data/store/schema.dart +++ b/lib/data/store/schema.dart @@ -52,7 +52,14 @@ abstract final class SchemaVersion { /// layout written before versioning existed, hence the starting point /// rather than v1 /// v3: Spi's flat SSH fields nested under `ssh` - static const current = 3; + /// v4: Hive boxes replaced by one encrypted SQLite database + /// + /// There is no migration registered for v2 -> v3 or v3 -> v4. Both are done + /// by `HiveImport`, which is the only code that reads a Hive box and so the + /// only place a pre-v3 record can be decoded at all; it records [current] + /// when it finishes. Every install therefore reaches SQLite already at v4, + /// and [migrate] has nothing to do until a v5 exists. + static const current = 4; /// Persisted locally, never included in a backup: it describes *this /// device's* storage, and restoring another device's number would make the diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index 74e71e41f7..46215b2ded 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -1,5 +1,4 @@ import 'package:fl_lib/fl_lib.dart'; -import 'package:hive_ce/hive.dart'; import 'package:meta/meta.dart'; import 'package:server_box/data/model/container/type.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; @@ -9,16 +8,13 @@ import 'package:server_box/data/store/private_key.dart'; import 'package:server_box/data/store/setting.dart'; import 'package:server_box/data/store/snippet.dart'; -class ServerStore extends CachedHiveStore { +class ServerStore extends CachedSqliteStore { ServerStore._() : super('server'); - /// The same seam [SettingStore.forBox] has: `init()` reaches for the - /// platform's secure storage to get an encryption cipher, which a unit test - /// has no implementation of. + /// The same seam [SettingStore.forTest] has: a distinct store name, so a test + /// on `SqliteDb.openInMemory()` cannot collide with another test's rows. @visibleForTesting - ServerStore.forBox(Box testBox) : super('server_test') { - box = testBox; - } + ServerStore.forTest() : super('server_test'); static final instance = ServerStore._(); @@ -102,7 +98,7 @@ class ServerStore extends CachedHiveStore { srvOrderChanged = true; } - final spi = get(newId); + final spi = fetchOneRaw(newId); if (spi != null) { final newSpi = _replaceJumpIds(spi, idMap); if (newSpi != null) { diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 4577b86316..004667d79e 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:fl_lib/fl_lib.dart'; -import 'package:hive_ce/hive.dart'; import 'package:meta/meta.dart'; import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/model/app/net_view.dart'; @@ -10,13 +9,13 @@ import 'package:server_box/data/model/app/tab.dart'; import 'package:server_box/data/model/ssh/virtual_key.dart'; import 'package:server_box/data/res/default.dart'; -class SettingStore extends HiveStore { +class SettingStore extends SqliteStore { SettingStore._() : super('setting'); + /// A distinct store name, so a test on `SqliteDb.openInMemory()` cannot + /// collide with another test's rows. @visibleForTesting - SettingStore.forBox(Box testBox) : super('setting_test') { - box = testBox; - } + SettingStore.forTest() : super('setting_test'); static final instance = SettingStore._(); @@ -329,7 +328,26 @@ class SettingStore extends HiveStore { /// whatever the last unversioned release wrote, and that is v2 (Spi with a /// flat SSH layout plus `monitorHttp`). A fresh install overwrites this with /// [SchemaVersion.current] before any migration runs. - late final schemaVersion = propertyDefault('schemaVersion', 2); + /// + /// An **internal** key, so `getAllMap` leaves it out of a backup and `clear` + /// leaves it alone. Under a plain key it travelled: restoring a backup taken + /// on a device still on the previous release wrote that device's version + /// back, and the next launch found a version with no migration registered for + /// it and threw `SchemaTooNewException`'s counterpart — a `StateError` that + /// nothing catches. + /// + /// Being internal also means it never stamps `lastUpdateTs`, which it must + /// not: it describes this device's storage, so counting a migration writing + /// it as a user edit would make a device that has only just upgraded claim + /// the newer copy of everything at the next sync. + /// + /// TODO: drop `schemaVersion` from `removeRetiredKeys` once no install can + /// still carry the plain-key copy this replaced. + late final schemaVersion = propertyDefault( + '${StoreDefaults.prefixKey}schemaVersion', + 2, + updateLastModified: false, + ); /// Hide title bar on desktop late final hideTitleBar = propertyDefault('hideTitleBar', isDesktop); @@ -425,12 +443,17 @@ class SettingStore extends HiveStore { ); /// Add Agent to the legacy default home tabs once. + /// + /// Written with `updateLastUpdateTsOnSet: false` throughout, as the Hive + /// version got by writing straight to the box: a migration this build runs on + /// its own is not an edit the user made, and counting it as one would have + /// every install claim a newer copy than whatever it last synced with. Future migrateHomeTabsAgent() async { const key = 'homeTabs'; const flagKey = 'homeTabsAgentMigrated'; - if (box.get(flagKey) == true) return; + if (get(flagKey) == true) return; - final tabs = AppTab.parseAppTabsFromObj(box.get(key)); + final tabs = AppTab.parseAppTabsFromObj(get(key)); const legacyDefaultTabs = { AppTab.server, AppTab.ssh, @@ -439,12 +462,13 @@ class SettingStore extends HiveStore { }; if (tabs.length == legacyDefaultTabs.length && tabs.toSet().containsAll(legacyDefaultTabs)) { - await box.put( + set( key, [...tabs, AppTab.agent].map((tab) => tab.name).toList(), + updateLastUpdateTsOnSet: false, ); } - await box.put(flagKey, true); + set(flagKey, true, updateLastUpdateTsOnSet: false); } /// Hide port forward beta warning @@ -469,7 +493,16 @@ class SettingStore extends HiveStore { /// installs are cleaned without another migration flag becoming permanent /// state of its own. Future removeRetiredKeys() async { - await box.deleteAll(const ['moveOutServerTabFuncBtns', 'forceSinglePane']); + for (final key in const [ + 'moveOutServerTabFuncBtns', + 'forceSinglePane', + // The plain-key schema version. It moved to an internal key so that a + // backup stops carrying it; this drops the copy a Hive import brought + // across, which nothing reads and a backup would still export. + 'schemaVersion', + ]) { + remove(key, updateLastUpdateTsOnRemove: false); + } } /// Migrate sshConnectionMode from old int values (-1/0/1) to bool. @@ -477,8 +510,8 @@ class SettingStore extends HiveStore { void migrateSshConnectionMode() { const key = 'sshConnectionMode'; const flagKey = 'sshConnectionModeMigrated'; - if (box.get(flagKey) == true) return; - final raw = box.get(key); + if (get(flagKey) == true) return; + final raw = get(key); if (raw is int) { // -1 = auto, 0 = built-in, 1 = system SSH final bool value; @@ -487,8 +520,8 @@ class SettingStore extends HiveStore { } else { value = raw != 0; } - box.put(key, value); + set(key, value, updateLastUpdateTsOnSet: false); } - box.put(flagKey, true); + set(flagKey, true, updateLastUpdateTsOnSet: false); } } diff --git a/lib/data/store/snippet.dart b/lib/data/store/snippet.dart index c8cd61a63b..c4b346f436 100644 --- a/lib/data/store/snippet.dart +++ b/lib/data/store/snippet.dart @@ -1,18 +1,14 @@ -import 'package:hive_ce/hive.dart'; import 'package:meta/meta.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/store/cached_store.dart'; -class SnippetStore extends CachedHiveStore { +class SnippetStore extends CachedSqliteStore { SnippetStore._() : super('snippet'); - /// The same seam [ServerStore.forBox] has: `init()` reaches for the - /// platform's secure storage to get an encryption cipher, which a unit test - /// has no implementation of. + /// The same seam [ServerStore.forTest] has: a distinct store name, so a test + /// on `SqliteDb.openInMemory()` cannot collide with another test's rows. @visibleForTesting - SnippetStore.forBox(Box testBox) : super('snippet_test') { - box = testBox; - } + SnippetStore.forTest() : super('snippet_test'); static final instance = SnippetStore._(); diff --git a/lib/main.dart b/lib/main.dart index 1f3c2b20c0..393929c69d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/session_manager.dart'; -import 'package:server_box/data/store/migrations/m002_nest_ssh.dart'; import 'package:server_box/data/store/schema.dart'; import 'package:server_box/data/store/server.dart'; import 'package:server_box/hive/hive_registrar.g.dart'; @@ -130,12 +129,15 @@ Future _initData() async { dirs: const {PathDir.img, PathDir.font}, ); + // Only so `HiveImport` can read the boxes an upgrading install still has. + // Nothing writes Hive any more. + // + // TODO: drop this, `lib/hive/` and the `hive_ce*` dependencies together with + // `HiveImport`, once no supported install can still be on Hive. await Hive.initFlutter(); Hive.registerAdapters(); // Reads pre-v3 server records, which the generated SpiAdapter no longer // understands (it owns a new typeId and the nested layout). - // TODO: drop together with SpiNestSshMigration once no install can still be - // on schema v2. Hive.registerAdapter(SpiLegacyAdapter()); await PrefStore.shared.init(); // Call this before accessing any store @@ -154,11 +156,12 @@ Future _initData() async { // start empty — an app that opens with nothing in it can still be told // what happened, one that does not open cannot. Loggers.app.warning('Stores.init after sandbox import', e, s); - // Closed before the files are deleted. `Stores.init` may have opened - // several boxes before the one that threw, and unlinking a `.hive` out - // from under a live handle is undefined at best: on Windows the delete - // fails outright and the copy stays, so the retry below reopens exactly - // the data that just failed to open. + // Closed before the files are deleted. `Stores.init` may have got as far as + // opening the database, or several boxes, before the one that threw, and + // unlinking a file out from under a live handle is undefined at best: on + // Windows the delete fails outright and the copy stays, so the retry below + // reopens exactly the data that just failed to open. + await SqliteDb.close(); await Hive.close(); await getIt.reset(); await SandboxImport.undo(); @@ -230,7 +233,12 @@ Future _doDbMigrate() async { // its current type. Throws SchemaTooNewException when the data was written // by a newer build — that must not be swallowed, since continuing would let // this build overwrite records whose shape it doesn't understand. - await SchemaVersion.migrate(const [SpiNestSshMigration()]); + // + // The list is empty as of v4: `HiveImport` brings every install straight to + // `current` while copying, because a pre-v3 record only exists as a Hive + // value and that is the one pass that reads one. The call stays for the + // downgrade check it performs, and for the next step that does exist. + await SchemaVersion.migrate(const []); // Then the app-level fixups, which read records as `Spi`. ServerStore.instance.migrateIds(); diff --git a/lib/src/rust/api/parser.dart b/lib/src/rust/api/parser.dart index 11f3250568..386e136fa9 100644 --- a/lib/src/rust/api/parser.dart +++ b/lib/src/rust/api/parser.dart @@ -1,5 +1,5 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import diff --git a/lib/src/rust/api/script.dart b/lib/src/rust/api/script.dart index 99b69ccdae..ca48eae865 100644 --- a/lib/src/rust/api/script.dart +++ b/lib/src/rust/api/script.dart @@ -1,5 +1,5 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index f105a75397..bac0f284bb 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -1,5 +1,5 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. // ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field @@ -65,7 +65,7 @@ class RustLib extends BaseEntrypoint { kDefaultExternalLibraryLoaderConfig; @override - String get codegenVersion => '2.12.0'; + String get codegenVersion => '2.13.0-beta.6'; @override int get rustContentHash => -397184763; diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index f338c56682..edaae6810d 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -1,5 +1,5 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. // ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index 1ff0fdb63c..286d1f5f31 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -1,5 +1,5 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.12.0. +// @generated by `flutter_rust_bridge`@ 2.13.0-beta.6. // ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field diff --git a/lib/view/page/private_key/list.dart b/lib/view/page/private_key/list.dart index 59bd3001d5..0868718cff 100644 --- a/lib/view/page/private_key/list.dart +++ b/lib/view/page/private_key/list.dart @@ -69,7 +69,7 @@ class _PrivateKeyListState extends ConsumerState extension on _PrivateKeyListState { void _autoAddSystemPriavteKey() async { // Only trigger on desktop platform and no private key saved - if (isDesktop && Stores.snippet.box.keys.isEmpty) { + if (isDesktop && Stores.key.keys().isEmpty) { final home = Pfs.homeDir; if (home == null) return; final idRsaFile = File(home.joinPath('.ssh/id_rsa')); diff --git a/lib/view/page/server/connection_stats.dart b/lib/view/page/server/connection_stats.dart index d1109af575..19ddf6c773 100644 --- a/lib/view/page/server/connection_stats.dart +++ b/lib/view/page/server/connection_stats.dart @@ -288,13 +288,12 @@ extension _Actions on _ConnectionStatsPageState { } Future _showCompactDialog() async { + // One file for every store now, so this is the whole database rather than + // this page's share of it — and so is the `VACUUM` the dialog runs. final oldSize = await Stores.connectionStats.dbSizeAsync(); if (!mounted) return; - final oldIndexSize = await Stores.connectionStats.indexDbSizeAsync(); - if (!mounted) return; - final totalSize = oldSize + oldIndexSize; - final sizeStr = totalSize.bytes2Str; + final sizeStr = oldSize.bytes2Str; context.showRoundDialog( title: l10n.compactDatabase, @@ -308,10 +307,7 @@ extension _Actions on _ConnectionStatsPageState { try { await Stores.connectionStats.compact(); final newSize = await Stores.connectionStats.dbSizeAsync(); - final newIndexSize = await Stores.connectionStats - .indexDbSizeAsync(); - final newTotalSize = newSize + newIndexSize; - final newSizeStr = newTotalSize.bytes2Str; + final newSizeStr = newSize.bytes2Str; _finishCompacting('${libL10n.success}: $sizeStr -> $newSizeStr'); } catch (e) { _finishCompacting('${libL10n.error}: $e'); diff --git a/lib/view/page/server/edit/actions.dart b/lib/view/page/server/edit/actions.dart index 40c587df2a..5175fcffdb 100644 --- a/lib/view/page/server/edit/actions.dart +++ b/lib/view/page/server/edit/actions.dart @@ -370,7 +370,7 @@ extension _Actions on _ServerEditPageState { } if (this.spi == null) { - final existsIds = ServerStore.instance.box.keys; + final existsIds = Stores.server.keys(); if (existsIds.contains(spi.id)) { Toast.show('${l10n.sameIdServerExist}: ${spi.id}'); return; diff --git a/lib/view/page/server/edit/edit.dart b/lib/view/page/server/edit/edit.dart index 15c950642b..0f5d191d79 100644 --- a/lib/view/page/server/edit/edit.dart +++ b/lib/view/page/server/edit/edit.dart @@ -23,7 +23,6 @@ import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/provider/private_key.dart'; import 'package:server_box/data/provider/server/all.dart'; import 'package:server_box/data/res/store.dart'; -import 'package:server_box/data/store/server.dart'; import 'package:server_box/view/page/private_key/edit.dart'; import 'package:server_box/view/page/server/custom_cmds.dart'; import 'package:server_box/view/widget/page_columns.dart'; diff --git a/lib/view/page/setting/entries/ai.dart b/lib/view/page/setting/entries/ai.dart index ad3dbd95bf..c49f24f754 100644 --- a/lib/view/page/setting/entries/ai.dart +++ b/lib/view/page/setting/entries/ai.dart @@ -5,7 +5,7 @@ extension _AI on _AppSettingsPageState { /// settings row that opens something: title, the current value under it, and /// a chevron saying there is more behind the tap. Widget _buildAskAiTextTile({ - required HiveProp prop, + required SqliteProp prop, required Widget leading, required String title, required String hint, @@ -141,7 +141,7 @@ extension _AI on _AppSettingsPageState { } Future _showAskAiFieldDialog({ - required HiveProp prop, + required SqliteProp prop, required String title, required String hint, String? description, diff --git a/lib/view/page/setting/entries/app.dart b/lib/view/page/setting/entries/app.dart index 7d6961f735..83ab00b6bc 100644 --- a/lib/view/page/setting/entries/app.dart +++ b/lib/view/page/setting/entries/app.dart @@ -494,14 +494,49 @@ extension _App on _AppSettingsPageState { return; } final encrypted = Cryptor.encrypt(json.encode(newSettings), pwd); - Stores.setting.box.put(encryptedKey, encrypted); + // Not stamping `lastUpdateTs`, which is what going straight to the + // box used to do. + // + // TODO: decide whether that was intentional. Editing the raw settings + // is a user edit, so leaving the timestamps alone means sync will not + // carry it to another device until something else is changed. + Stores.setting.set( + encryptedKey, + encrypted, + updateLastUpdateTsOnSet: false, + ); } else { - Stores.setting.box.putAll(newSettings); + // One transaction, as `Backup.merge` does: this rewrites the whole + // settings store, and half of an edit is not a state to leave behind. + SqliteStore.transact(() { + for (final entry in newSettings.entries) { + final value = entry.value; + // A key set to null means "clear this". Skipping it instead left + // the previous value in place, and the key being present kept it + // out of `removedKeys` below too — so the edit reported success and + // changed nothing. + if (value == null) { + Stores.setting.remove(entry.key, updateLastUpdateTsOnRemove: false); + continue; + } + Stores.setting.set( + entry.key, + value as Object, + updateLastUpdateTsOnSet: false, + ); + } final newKeys = newSettings.keys.toSet(); - final removedKeys = initialKeys.where((e) => !newKeys.contains(e)); + // Internal keys are shown by the editor (it reads with + // `includeInternalKeys: true`) but are not the user's to delete: one + // of them records that the Hive import already ran, and dropping it + // makes the next launch copy the retained boxes back over everything. + final removedKeys = initialKeys.where( + (e) => !newKeys.contains(e) && !Stores.setting.isInternalKey(e), + ); for (final key in removedKeys) { - Stores.setting.box.delete(key); + Stores.setting.remove(key, updateLastUpdateTsOnRemove: false); } + }); } } catch (e, trace) { context.showRoundDialog( diff --git a/lib/view/page/setting/entries/editor.dart b/lib/view/page/setting/entries/editor.dart index 74e15fb7aa..048442a24b 100644 --- a/lib/view/page/setting/entries/editor.dart +++ b/lib/view/page/setting/entries/editor.dart @@ -1,7 +1,7 @@ part of '../entry.dart'; extension _Editor on _AppSettingsPageState { - Future _pickEditorTheme(HiveProp property) async { + Future _pickEditorTheme(SqliteProp property) async { final selected = await context.showPickSingleDialog( title: libL10n.theme, items: themeMap.keys.toList(), @@ -102,7 +102,7 @@ extension _Editor on _AppSettingsPageState { ); } - void _showFontFamilyDialog(HiveProp property) { + void _showFontFamilyDialog(SqliteProp property) { showTextSettingDialog( title: libL10n.font, initialValue: property.fetch() ?? '', @@ -113,7 +113,7 @@ extension _Editor on _AppSettingsPageState { ); } - void _showFontSizeDialog(HiveProp property) { + void _showFontSizeDialog(SqliteProp property) { final ctrl = TextEditingController(text: property.fetch().toString()); void onSave() { context.popDialog(); diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index 4496d5fd4e..d3e20a2587 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -95,8 +95,7 @@ class _SettingsPageState extends ConsumerState { } void _clearAllSettings() { - final keys = SettingStore.instance.box.keys; - SettingStore.instance.box.deleteAll(keys); + SettingStore.instance.clear(); Toast.success(libL10n.success); } diff --git a/lib/view/page/setting/platform/ios.dart b/lib/view/page/setting/platform/ios.dart index da54350838..8e8aad7883 100644 --- a/lib/view/page/setting/platform/ios.dart +++ b/lib/view/page/setting/platform/ios.dart @@ -168,7 +168,7 @@ extension _Actions on _IosSettingsPageState { Spi? get _accessoryServer { final id = Stores.setting.accessoryWidgetServerId.fetch(); - return id.isEmpty ? null : Stores.server.get(id); + return id.isEmpty ? null : Stores.server.fetchOneRaw(id); } void _onTapAccessoryWidgetServer() async { diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 9a75aad584..1d8b825d6c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,9 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - flutter_pty jni - sbm_ffi ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index 4b81f9b2d2..c2efd0b608 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index 5caa9d1579..c2efd0b608 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index 165b114a42..0000000000 --- a/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -platform :osx, '12.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - # target 'RunnerTests' do - # inherit! :search_paths - # end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/macos/Podfile.lock b/macos/Podfile.lock deleted file mode 100644 index 541ca60c0b..0000000000 --- a/macos/Podfile.lock +++ /dev/null @@ -1,28 +0,0 @@ -PODS: - - flutter_pty (0.0.1): - - FlutterMacOS - - FlutterMacOS (1.0.0) - - sbm_ffi (0.0.1): - - FlutterMacOS - -DEPENDENCIES: - - flutter_pty (from `Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos`) - - FlutterMacOS (from `Flutter/ephemeral`) - - sbm_ffi (from `Flutter/ephemeral/.symlinks/plugins/sbm_ffi/macos`) - -EXTERNAL SOURCES: - flutter_pty: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos - FlutterMacOS: - :path: Flutter/ephemeral - sbm_ffi: - :path: Flutter/ephemeral/.symlinks/plugins/sbm_ffi/macos - -SPEC CHECKSUMS: - flutter_pty: 1e360feaf8b1a213d2d50da3c076005b8eef1eee - FlutterMacOS: c232990155153907050900a2e175c7773903ba4e - sbm_ffi: a03f641ae260971838739d773a55cddda61ba73e - -PODFILE CHECKSUM: 2190e15ede1572f0b104dd8ad4ae165b26ca5bfa - -COCOAPODS: 1.17.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 992680647e..13dad5d8c9 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - CB890EE4310DAC9F20C4B22D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4428BFC11DD7EDA01918FBB /* Pods_Runner.framework */; }; E372ECA42BB94B360078B9D4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E372ECA32BB94B360078B9D4 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -55,8 +54,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 15B02E741770D3594400C89E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 18C5D7F664081E15D4F73847 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; @@ -72,15 +69,9 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 61D014A5BEAA3F969F06061C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 7E4B8E0435A8CD429FA930EA /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 97047891D5556990B2D8BDF7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - B4428BFC11DD7EDA01918FBB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BFB2C4CAFA221419C2D3A5FB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - C80488AEAD11AE69952819A3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; E372ECA32BB94B360078B9D4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; E3D26BD42B99689700D83425 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = ""; }; E3D26BD52B99689B00D83425 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.strings"; sourceTree = ""; }; @@ -99,7 +90,6 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - CB890EE4310DAC9F20C4B22D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -132,8 +122,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - B8A24A574E1520AC66526B2B /* Pods */, ); sourceTree = ""; }; @@ -182,28 +170,6 @@ path = Runner; sourceTree = ""; }; - B8A24A574E1520AC66526B2B /* Pods */ = { - isa = PBXGroup; - children = ( - 18C5D7F664081E15D4F73847 /* Pods-Runner.debug.xcconfig */, - C80488AEAD11AE69952819A3 /* Pods-Runner.release.xcconfig */, - BFB2C4CAFA221419C2D3A5FB /* Pods-Runner.profile.xcconfig */, - 7E4B8E0435A8CD429FA930EA /* Pods-RunnerTests.debug.xcconfig */, - 97047891D5556990B2D8BDF7 /* Pods-RunnerTests.release.xcconfig */, - 15B02E741770D3594400C89E /* Pods-RunnerTests.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B4428BFC11DD7EDA01918FBB /* Pods_Runner.framework */, - 61D014A5BEAA3F969F06061C /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -211,14 +177,12 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 906584B20D0BBB0E924799BF /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, A1B2C3D4E5F60718293A4B5C /* Fix Objective-C Framework Resources */, - D031AF8C21824AF8F0134BDE /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -341,28 +305,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 906584B20D0BBB0E924799BF /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; A1B2C3D4E5F60718293A4B5C /* Fix Objective-C Framework Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -381,23 +323,6 @@ shellPath = /bin/sh; shellScript = "FRAMEWORK=\"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/objective_c.framework\"\nif [ -d \"$FRAMEWORK/Versions\" ]; then\n if [ ! -L \"$FRAMEWORK/Versions/Current\" ]; then\n (cd \"$FRAMEWORK/Versions\" && ln -sf A Current)\n fi\n rm -f \"$FRAMEWORK/Resources\"\n ln -sf Versions/Current/Resources \"$FRAMEWORK/Resources\"\nfi\n"; }; - D031AF8C21824AF8F0134BDE /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc14c7..1d526a16ed 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/packages/fl_lib b/packages/fl_lib index 9894975a91..458b54ccb1 160000 --- a/packages/fl_lib +++ b/packages/fl_lib @@ -1 +1 @@ -Subproject commit 9894975a91576820cdd6299fb27a8aa72e6c7e7a +Subproject commit 458b54ccb1fce59c316cdb27af4381f0a35c8cd0 diff --git a/packages/flutter_pty b/packages/flutter_pty new file mode 160000 index 0000000000..792a46a738 --- /dev/null +++ b/packages/flutter_pty @@ -0,0 +1 @@ +Subproject commit 792a46a738b689a208163909a2ffa902d07d22ad diff --git a/pubspec.lock b/pubspec.lock index ac67467864..5b94beae29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -584,10 +584,9 @@ packages: flutter_pty: dependency: "direct main" description: - name: flutter_pty - sha256: c2f3b3160b519ac820fa3f6ef175361f2dfc52c557465643589542e9f229ad66 - url: "https://pub.dev" - source: hosted + path: "packages/flutter_pty" + relative: true + source: path version: "0.4.2" flutter_riverpod: dependency: "direct main" @@ -601,10 +600,18 @@ packages: dependency: "direct main" description: name: flutter_rust_bridge - sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a + sha256: "7e8f565714dcb06197d29c74a559ee826c4eab4d586d7ccf9826aa5dc9478015" + url: "https://pub.dev" + source: hosted + version: "2.13.0-beta.6" + flutter_rust_bridge_hooks: + dependency: "direct main" + description: + name: flutter_rust_bridge_hooks + sha256: a950952d2de2ffc2464e336bd04fe16124b863a27ed547c41b94ad635d07bfd1 url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0-beta.6" flutter_secure_storage: dependency: transitive description: @@ -1074,6 +1081,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.2" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: a1c26117c48cebe5677b0cf0e33a980a79a7c5577effc86f52e5a0d309cdcb60 + url: "https://pub.dev" + source: hosted + version: "0.19.3" + native_toolchain_rust: + dependency: transitive + description: + name: native_toolchain_rust + sha256: faa57d2258a3b0fd2a634054f54e4496c9fcbd971977e7d2b7e6916d56892857 + url: "https://pub.dev" + source: hosted + version: "1.0.4+0" nested: dependency: transitive description: @@ -1386,13 +1409,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.4" - sbm_ffi: - dependency: "direct main" - description: - path: "crates/sbm_ffi" - relative: true - source: path - version: "0.0.1" screen_retriever: dependency: transitive description: @@ -1582,6 +1598,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: "64b2c63c8232dd20d14b34105a81ebfd74320442e8451f836179ec89986aa478" + url: "https://pub.dev" + source: hosted + version: "3.5.1" stack_trace: dependency: transitive description: @@ -1662,6 +1686,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.18" + toml: + dependency: transitive + description: + name: toml + sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e" + url: "https://pub.dev" + source: hosted + version: "0.18.0" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 381d280549..ec890f4be4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,6 @@ dependencies: flutter_highlight: ^0.7.0 flutter_displaymode: ^0.7.0 flutter_markdown_plus: ^1.0.12 - flutter_pty: ^0.4.2 flutter_svg: ^2.2.1 fl_chart: ^1.2.0 freezed_annotation: ^3.0.0 @@ -38,6 +37,10 @@ dependencies: responsive_framework: ^1.5.1 riverpod: ^3.2.1 riverpod_annotation: ^4.0.2 + # Direct, though `SqliteStore` lives in fl_lib: `hooks.user_defines` below is + # only honoured for this package's own dependencies, and the Hive migration + # opens the database from here too. + sqlite3: ^3.5.1 wakelock_plus: ^1.7.0 wake_on_lan: ^4.1.1+3 webdav_client_plus: ^2.0.0 @@ -62,9 +65,14 @@ dependencies: path: packages/plain_notification_token fl_lib: path: packages/fl_lib - sbm_ffi: - path: crates/sbm_ffi - flutter_rust_bridge: 2.12.0 + # Forked to swap five per-platform build integrations for one build hook, + # which is what takes the last pod out of the iOS and macOS builds. + flutter_pty: + path: packages/flutter_pty + flutter_rust_bridge: 2.13.0-beta.6 + # Drives the build hook below. `sbm_ffi` is no longer a Flutter plugin — the + # hook compiles the crate directly, so there is nothing to depend on. + flutter_rust_bridge_hooks: 2.13.0-beta.6 desktop_drop: ^0.7.1 dev_dependencies: @@ -83,6 +91,19 @@ dev_dependencies: fl_build: path: packages/fl_build +# Which SQLite gets compiled into the app. Only the root package can set this — +# a dependency cannot supply its own defaults — so it lives here even though +# every caller is in fl_lib. +# +# `sqlite3mc` rather than `sqlcipher`: both encrypt the whole file, keys and +# indexes included, but SQLCipher links OpenSSL on Windows, Linux and Android, +# where SQLite3MultipleCiphers carries its cipher implementations in its own +# source and needs nothing installed on any of the five platforms. +hooks: + user_defines: + sqlite3: + source: sqlite3mc + flutter: generate: true uses-material-design: true diff --git a/test/agent_conversation_store_test.dart b/test/agent_conversation_store_test.dart index bab34c7ab9..4064f2a0ee 100644 --- a/test/agent_conversation_store_test.dart +++ b/test/agent_conversation_store_test.dart @@ -1,31 +1,19 @@ -import 'dart:io'; - +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/model/ai/agent_conversation.dart'; import 'package:server_box/data/model/ai/ask_ai_models.dart'; import 'package:server_box/data/store/agent_conversation.dart'; void main() { - late Directory tempDir; - late Box box; late AgentConversationStore store; - setUpAll(() async { - tempDir = await Directory.systemTemp.createTemp('server-box-agent-test-'); - Hive.init(tempDir.path); - box = await Hive.openBox('agent_conversation_test'); - store = AgentConversationStore.forBox(box); - }); - setUp(() async { - await box.clear(); + SqliteDb.openInMemory(); + store = AgentConversationStore.forTest(); + await store.init(); }); - tearDownAll(() async { - await box.close(); - await tempDir.delete(recursive: true); - }); + tearDown(SqliteDb.close); test('round-trips protocol-complete conversation items', () { const command = AskAiCommand( @@ -254,4 +242,85 @@ void main() { expect(store.fetchForServer('server-a'), isEmpty); expect(store.fetchForServer('server-b').single.id, other.id); }); + + group('the queries the tables replaced a full scan with', () { + AgentConversation conv(String id, String serverId, DateTime updatedAt) => + AgentConversation( + id: id, + serverId: serverId, + title: 't-$id', + createdAt: updatedAt, + updatedAt: updatedAt, + protocol: AskAiProtocol.responses, + providerBaseUrl: 'https://x', + model: 'm', + items: const [], + ); + + final base = DateTime.fromMillisecondsSinceEpoch(1000); + + test('a server list comes back newest first', () { + store.save(conv('a1', 'srv-a', base), setActive: false); + store.save( + conv('a3', 'srv-a', base.add(const Duration(minutes: 2))), + setActive: false, + ); + store.save( + conv('a2', 'srv-a', base.add(const Duration(minutes: 1))), + setActive: false, + ); + + // Ordered by the index rather than by an in-memory sort of every + // conversation in the app, which is what the K-V version did. + expect(store.fetchForServer('srv-a').map((e) => e.id), [ + 'a3', + 'a2', + 'a1', + ]); + }); + + test('the active conversation is per server', () { + store.save(conv('a1', 'srv-a', base)); + store.save(conv('b1', 'srv-b', base)); + + expect(store.activeConversationId('srv-a'), 'a1'); + expect(store.activeConversationId('srv-b'), 'b1'); + expect(store.fetchActive('srv-a')?.id, 'a1'); + }); + + test('another server cannot be made active on this one', () { + store.save(conv('a1', 'srv-a', base), setActive: false); + expect(store.setActive('srv-b', 'a1'), isFalse); + expect(store.activeConversationId('srv-b'), isNull); + }); + + test('deleting the last conversation leaves nothing active', () { + store.save(conv('a1', 'srv-a', base)); + store.deleteConversation('srv-a', 'a1'); + expect(store.activeConversationId('srv-a'), isNull); + }); + + test('clearing a server leaves the others alone', () { + store.save(conv('a1', 'srv-a', base)); + store.save(conv('b1', 'srv-b', base)); + + store.clearServer('srv-a'); + + expect(store.fetchForServer('srv-a'), isEmpty); + expect(store.activeConversationId('srv-a'), isNull); + expect(store.fetchForServer('srv-b'), hasLength(1)); + expect(store.activeConversationId('srv-b'), 'b1'); + }); + + test('a write fires the change stream', () async { + final seen = []; + final sub = store.watch().listen(seen.add); + addTearDown(sub.cancel); + + store.save(conv('a1', 'srv-a', base)); + await Future.delayed(Duration.zero); + + expect(seen, hasLength(1)); + }); + }); } diff --git a/test/agent_shell_view_test.dart b/test/agent_shell_view_test.dart index 71289fe1bb..44bb124359 100644 --- a/test/agent_shell_view_test.dart +++ b/test/agent_shell_view_test.dart @@ -1,11 +1,9 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/core/extension/context/locale.dart' as app_locale; import 'package:server_box/data/model/ai/ask_ai_models.dart'; import 'package:server_box/data/provider/ai/agent_session.dart'; @@ -30,36 +28,22 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box conversationBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-agent-shell-'); - Hive.init(tempDir.path); - // In memory rather than on disk. A widget test body runs in a fake-async - // zone; a real file write started there completes on a callback that zone - // is no longer pumping, so the box's write lock is never released and - // closing it in `tearDown` blocks — which hangs the whole run, not just - // this file. The shell persists its mode the moment it changes, so every - // test here writes. - settingBox = await Hive.openBox( - 'setting_test', - bytes: Uint8List(0), - ); - conversationBox = await Hive.openBox( - 'agent_conversation_test', - bytes: Uint8List(0), - ); - getIt.registerSingleton(SettingStore.forBox(settingBox)); + SqliteDb.openInMemory(); + // In memory rather than on disk: the shell persists its mode the moment it + // changes, so every test here writes, and none of them should leave a + // database behind. + getIt.registerSingleton(SettingStore.forTest()); getIt.registerSingleton( - AgentConversationStore.forBox(conversationBox), + AgentConversationStore.forTest()..init(), ); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); - await conversationBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/agent_view_test.dart b/test/agent_view_test.dart index 0d5ab631f7..c44cf6b2d8 100644 --- a/test/agent_view_test.dart +++ b/test/agent_view_test.dart @@ -1,10 +1,9 @@ import 'dart:io'; -import 'dart:typed_data'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:riverpod/misc.dart' show Override; import 'package:server_box/core/extension/context/locale.dart' as app_locale; import 'package:server_box/data/model/ai/agent_conversation.dart'; @@ -36,35 +35,21 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box conversationBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-agent-view-'); - Hive.init(tempDir.path); - // In memory: the return-key tests below write the setting they are - // about, and a real file write started inside a `testWidgets` body - // completes on a callback the fake-async zone is no longer pumping — - // so the box's write lock is never released and `close()` in tearDown - // blocks forever, with no failure to say which file did it. - settingBox = await Hive.openBox( - 'setting_test', - bytes: Uint8List(0), - ); - conversationBox = await Hive.openBox( - 'agent_conversation_test', - bytes: Uint8List(0), - ); - getIt.registerSingleton(SettingStore.forBox(settingBox)); + SqliteDb.openInMemory(); + // In memory: the return-key tests below write the setting they are about, + // and none of them should leave a database behind. + getIt.registerSingleton(SettingStore.forTest()); getIt.registerSingleton( - AgentConversationStore.forBox(conversationBox), + AgentConversationStore.forTest()..init(), ); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); - await conversationBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/app_locale_test.dart b/test/app_locale_test.dart index b0ab6ffb8f..adbd81f7f1 100644 --- a/test/app_locale_test.dart +++ b/test/app_locale_test.dart @@ -1,31 +1,25 @@ -import 'dart:io'; - +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/app.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/setting.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - - late Directory tempDir; - late Box box; late SettingStore setting; setUp(() async { - tempDir = await Directory.systemTemp.createTemp('server-box-app-test-'); - Hive.init(tempDir.path); - box = await Hive.openBox('setting_test'); - setting = SettingStore.forBox(box); + SqliteDb.openInMemory(); + setting = SettingStore.forTest(); getIt.registerSingleton(setting); FlutterSecureStorage.setMockInitialValues({}); }); tearDown(() async { await getIt.reset(); + await SqliteDb.close(); }); testWidgets('updates the onboarding locale when the setting changes', ( diff --git a/test/connection_stats_store_test.dart b/test/connection_stats_store_test.dart new file mode 100644 index 0000000000..346ffba4bf --- /dev/null +++ b/test/connection_stats_store_test.dart @@ -0,0 +1,258 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:server_box/data/model/server/connection_stat.dart'; +import 'package:server_box/data/store/connection_stats.dart'; + +/// The bounds this table keeps itself inside, which used to be four hand-written +/// passes over a K-V store and are now two `DELETE`s. +void main() { + late ConnectionStatsStore store; + + /// Anchored to now, not to a literal date: rows older than 30 days are + /// dropped on the next write, so a fixed date silently empties the table as + /// soon as it is more than a month in the past. + /// Truncated to milliseconds, which is what the column holds. A connection + /// attempt does not need microseconds, but a test comparing `DateTime`s does + /// need to compare the same precision. + final base = DateTime.fromMillisecondsSinceEpoch( + DateTime.now() + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch, + ); + + setUp(() async { + SqliteDb.openInMemory(); + store = ConnectionStatsStore.instance; + await store.init(); + await store.clearAll(); + }); + + tearDown(SqliteDb.close); + + ConnectionStat stat( + String serverId, { + required DateTime at, + ConnectionResult result = ConnectionResult.success, + String name = 'srv', + }) => ConnectionStat( + serverId: serverId, + serverName: name, + timestamp: at, + result: result, + durationMs: 1, + ); + + test('history comes back newest first', () async { + for (var i = 0; i < 3; i++) { + await store.recordConnection( + stat('a', at: base.add(Duration(minutes: i))), + ); + } + + final history = store.getConnectionHistory('a'); + expect(history.length, 3); + expect(history.first.timestamp, base.add(const Duration(minutes: 2))); + expect(history.last.timestamp, base); + }); + + test('one server does not see another', () async { + await store.recordConnection(stat('a', at: base)); + await store.recordConnection(stat('b', at: base)); + + expect(store.getConnectionHistory('a').single.serverId, 'a'); + expect(store.getConnectionHistory('b').single.serverId, 'b'); + }); + + test('a server keeps its newest 100 and no more', () async { + for (var i = 0; i < 130; i++) { + await store.recordConnection( + stat('a', at: base.add(Duration(minutes: i))), + ); + } + + final history = store.getConnectionHistory('a'); + expect(history.length, 100); + // The 30 oldest went, not an arbitrary 30. + expect(history.last.timestamp, base.add(const Duration(minutes: 30))); + expect(history.first.timestamp, base.add(const Duration(minutes: 129))); + }); + + test('the cap is per server, not overall', () async { + for (var i = 0; i < 100; i++) { + await store.recordConnection( + stat('a', at: base.add(Duration(minutes: i))), + ); + } + await store.recordConnection(stat('b', at: base)); + + expect(store.getConnectionHistory('a').length, 100); + expect(store.getConnectionHistory('b').length, 1); + }); + + test('anything older than 30 days is swept at init', () async { + // The age bound is not applied per write: recording happens on every + // connection attempt against every server, and paying for a whole-table + // sweep each time bought nothing in the common case where nothing has + // expired. It runs once per launch instead. + await store.recordConnection( + stat('a', at: DateTime.now().subtract(const Duration(days: 31))), + ); + await store.recordConnection(stat('a', at: base)); + expect(store.getConnectionHistory('a'), hasLength(2)); + + await store.init(); + + final kept = store.getConnectionHistory('a'); + expect(kept, hasLength(1)); + expect(kept.single.timestamp, base); + }); + + test('recording the same attempt twice does not double it', () async { + final at = base; + await store.recordConnection(stat('a', at: at)); + await store.recordConnection(stat('a', at: at, name: 'renamed')); + + final history = store.getConnectionHistory('a'); + expect(history.length, 1); + expect(history.single.serverName, 'renamed'); + }); + + test('the summary counts both outcomes', () async { + await store.recordConnection(stat('a', at: base)); + await store.recordConnection( + stat( + 'a', + at: base.add(const Duration(minutes: 1)), + result: ConnectionResult.timeout, + ), + ); + await store.recordConnection( + stat('a', at: base.add(const Duration(minutes: 2))), + ); + + final summary = store.getServerStats('a', 'srv'); + expect(summary.totalAttempts, 3); + expect(summary.successCount, 2); + expect(summary.failureCount, 1); + expect(summary.successRate, closeTo(2 / 3, 1e-9)); + expect(summary.lastSuccessTime, base.add(const Duration(minutes: 2))); + expect(summary.lastFailureTime, base.add(const Duration(minutes: 1))); + }); + + test('a server with no attempts summarises as empty, not as an error', () { + final summary = store.getServerStats('nobody', 'srv'); + expect(summary.totalAttempts, 0); + expect(summary.successRate, 0.0); + expect(summary.recentConnections, isEmpty); + }); + + test('the overall list names each server as it was named last', () async { + await store.recordConnection(stat('a', at: base, name: 'old-name')); + await store.recordConnection( + stat('a', at: base.add(const Duration(minutes: 1)), name: 'new-name'), + ); + await store.recordConnection(stat('b', at: base, name: 'other')); + + final all = store.getAllServerStats(); + expect(all.length, 2); + expect( + all.firstWhere((e) => e.serverId == 'a').serverName, + 'new-name', + reason: 'a rename leaves the old name on the older rows', + ); + }); + + test('clearing one server leaves the others', () async { + await store.recordConnection(stat('a', at: base)); + await store.recordConnection(stat('b', at: base)); + + await store.clearServerStats('a'); + + expect(store.getConnectionHistory('a'), isEmpty); + expect(store.getConnectionHistory('b'), hasLength(1)); + }); + + test('clearing everything leaves nothing', () async { + await store.recordConnection(stat('a', at: base)); + await store.clearAll(); + expect(store.getAllServerStats(), isEmpty); + }); + + group('the overall list, which is two queries regardless of server count', () { + test('it agrees with reading each server separately', () async { + for (var server = 0; server < 4; server++) { + for (var i = 0; i < 25; i++) { + await store.recordConnection( + stat( + 's$server', + at: base.add(Duration(minutes: i)), + result: i.isEven + ? ConnectionResult.success + : ConnectionResult.timeout, + name: 'name-$server', + ), + ); + } + } + + final all = { + for (final e in store.getAllServerStats()) e.serverId: e, + }; + expect(all.keys, hasLength(4)); + + // The aggregate has to answer exactly what the per-server read does. + for (var server = 0; server < 4; server++) { + final id = 's$server'; + final one = store.getServerStats(id, 'name-$server'); + final many = all[id]!; + + expect(many.serverName, one.serverName, reason: id); + expect(many.totalAttempts, one.totalAttempts, reason: id); + expect(many.successCount, one.successCount, reason: id); + expect(many.failureCount, one.failureCount, reason: id); + expect(many.successRate, closeTo(one.successRate, 1e-12), reason: id); + expect(many.lastSuccessTime, one.lastSuccessTime, reason: id); + expect(many.lastFailureTime, one.lastFailureTime, reason: id); + expect( + many.recentConnections.map((e) => e.timestamp), + one.recentConnections.map((e) => e.timestamp), + reason: id, + ); + } + }); + + test('each server carries at most 20 recent attempts, newest first', () async { + for (var i = 0; i < 40; i++) { + await store.recordConnection( + stat('a', at: base.add(Duration(minutes: i))), + ); + } + + final recent = store.getAllServerStats().single.recentConnections; + expect(recent, hasLength(20)); + expect(recent.first.timestamp, base.add(const Duration(minutes: 39))); + expect(recent.last.timestamp, base.add(const Duration(minutes: 20))); + }); + + test('a rename shows the newest name, not the oldest row\'s', () async { + await store.recordConnection(stat('a', at: base, name: 'before')); + await store.recordConnection( + stat('a', at: base.add(const Duration(minutes: 1)), name: 'after'), + ); + + expect(store.getAllServerStats().single.serverName, 'after'); + }); + + test('a server with only failures reports no last success', () async { + await store.recordConnection( + stat('a', at: base, result: ConnectionResult.authFailed), + ); + + final summary = store.getAllServerStats().single; + expect(summary.successCount, 0); + expect(summary.successRate, 0.0); + expect(summary.lastSuccessTime, isNull); + expect(summary.lastFailureTime, base); + }); + }); +} diff --git a/test/file_browser_test.dart b/test/file_browser_test.dart index ab5ca83161..b721b79d0b 100644 --- a/test/file_browser_test.dart +++ b/test/file_browser_test.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/model/file/file_backend.dart'; import 'package:server_box/data/model/file/file_ref.dart'; import 'package:server_box/data/res/store.dart'; @@ -96,18 +95,16 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-browser-'); - Hive.init(tempDir.path); - settingBox = await Hive.openBox('setting_test'); - getIt.registerSingleton(SettingStore.forBox(settingBox)); + SqliteDb.openInMemory(); + getIt.registerSingleton(SettingStore.forTest()); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/file_tab_restore_test.dart b/test/file_tab_restore_test.dart index c53107b174..589cec990b 100644 --- a/test/file_tab_restore_test.dart +++ b/test/file_tab_restore_test.dart @@ -1,11 +1,10 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/history.dart'; import 'package:server_box/data/store/private_key.dart'; @@ -32,25 +31,16 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box serverBox; - late Box keyBox; - late Box historyBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-filetab-'); - Hive.init(tempDir.path); - // In memory: this page saves on every change, and a file write started in - // a `testWidgets` body never releases the box's lock — `close()` in - // `tearDown` then hangs the whole run with no output. - settingBox = await Hive.openBox('setting_test', bytes: Uint8List(0)); - serverBox = await Hive.openBox('server_test', bytes: Uint8List(0)); - keyBox = await Hive.openBox('key_test', bytes: Uint8List(0)); - historyBox = await Hive.openBox('history_test', bytes: Uint8List(0)); - getIt.registerSingleton(SettingStore.forBox(settingBox)); - getIt.registerSingleton(ServerStore.forBox(serverBox)); - getIt.registerSingleton(PrivateKeyStore.forBox(keyBox)); - getIt.registerSingleton(HistoryStore.forBox(historyBox)); + SqliteDb.openInMemory(); + // In memory: this tree writes as it builds, and a test has no + // business leaving a database behind. + getIt.registerSingleton(SettingStore.forTest()); + getIt.registerSingleton(ServerStore.forTest()); + getIt.registerSingleton(PrivateKeyStore.forTest()); + getIt.registerSingleton(HistoryStore.forTest()); // A restored server session opens its browser, which now connects rather // than reporting that it is not connected — so an unreachable fixture // leaves a timer running. One second, pumped past below. @@ -59,10 +49,7 @@ void main() { tearDown(() async { await getIt.reset(); - await settingBox.close(); - await serverBox.close(); - await keyBox.close(); - await historyBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/file_transfer_test.dart b/test/file_transfer_test.dart index 73beda0a3f..bcea61cff4 100644 --- a/test/file_transfer_test.dart +++ b/test/file_transfer_test.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/model/file/file_ref.dart'; import 'package:server_box/data/model/file/transfer.dart'; import 'package:server_box/data/model/file/transfer_status.dart'; @@ -45,28 +45,20 @@ void main() { group('where a transfer runs', () { late Directory tempDir; - late Box settingBox; - late Box serverBox; - late Box keyBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-transfer-'); - Hive.init(tempDir.path); - settingBox = await Hive.openBox('setting_test'); - serverBox = await Hive.openBox('server_test'); - keyBox = await Hive.openBox('key_test'); - getIt.registerSingleton(SettingStore.forBox(settingBox)); + SqliteDb.openInMemory(); + getIt.registerSingleton(SettingStore.forTest()); // Building an `SftpFileRef` reads keys and jump servers out of these, // which is the work these tests are checking happens on this side. - getIt.registerSingleton(ServerStore.forBox(serverBox)); - getIt.registerSingleton(PrivateKeyStore.forBox(keyBox)); + getIt.registerSingleton(ServerStore.forTest()); + getIt.registerSingleton(PrivateKeyStore.forTest()); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); - await serverBox.close(); - await keyBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/hive_import_test.dart b/test/hive_import_test.dart new file mode 100644 index 0000000000..317466b904 --- /dev/null +++ b/test/hive_import_test.dart @@ -0,0 +1,323 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:server_box/data/model/container/type.dart'; +import 'package:server_box/data/model/server/connection_stat.dart'; +import 'package:server_box/data/model/server/private_key_info.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/server/snippet.dart'; +import 'package:server_box/data/model/server/ssh_credential.dart'; +import 'package:server_box/data/res/store.dart'; +import 'package:server_box/data/store/schema.dart'; +import 'package:server_box/hive/hive_registrar.g.dart'; +import 'package:server_box/hive/spi_legacy_adapter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// What an existing install's data looks like after the move off Hive. +/// +/// This is the one pass over a user's real records, and it is not repeatable — +/// once the marker is written the boxes are never read again. So it is worth +/// checking against boxes written the way the app wrote them, through +/// `HiveStore`, rather than against a hand-built map. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late Directory tempDir; + + setUpAll(() async { + tempDir = await Directory.systemTemp.createTemp('sbm-hive-import-'); + // `HiveStore.boxDir` resolves to this on an unsandboxed desktop build, and + // it is `late final`, so this stands in for `Paths.init` — which would need + // path_provider and would write into the real documents directory. + Paths.doc = tempDir.path; + + // One seeded generator reused, not a new one per byte — `Random(1)` inside + // the closure would have produced the same value 32 times. + final rng = Random(1); + FlutterSecureStorage.setMockInitialValues({ + 'hivePwd': base64UrlEncode( + Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256))), + ), + }); + SharedPreferences.setMockInitialValues({}); + await PrefStore.shared.init(); + + Hive.init(tempDir.path); + Hive.registerAdapters(); + Hive.registerAdapter(SpiLegacyAdapter()); + }); + + tearDownAll(() async { + await Hive.close(); + await tempDir.delete(recursive: true); + }); + + setUp(() async { + SqliteDb.openInMemory(); + }); + + tearDown(() async { + await getIt.reset(); + await SqliteDb.close(); + await Hive.close(); + // By file rather than `Hive.deleteFromDisk`, which only knows about boxes + // the current instance still has open — `seedHive` closes them all. + for (final f in tempDir.listSync()) { + if (f is File) f.deleteSync(); + } + Hive.init(tempDir.path); + }); + + /// Fills the boxes the way a running app would have left them. + Future seedHive() async { + Future open(String name) async { + final store = HiveStore(name); + await store.init(); + return store; + } + + final server = await open('server'); + await server.box.put( + 'srv-1', + const Spi( + id: 'srv-1', + name: 'prod', + ssh: SshCredential(ip: '10.0.0.1', user: 'root', port: 2222), + tags: ['a'], + ), + ); + + final snippet = await open('snippet'); + await snippet.box.put('uptime', const Snippet(name: 'uptime', script: 'w')); + + final key = await open('key'); + await key.box.put( + 'k1', + const PrivateKeyInfo(id: 'k1', key: 'PRIVATE'), + ); + + final setting = await open('setting'); + await setting.box.put('timeOut', 9); + await setting.box.put('recordHistory', false); + await setting.box.put('homeTabs', ['server', 'ssh']); + + final history = await open('history'); + await history.box.put('sftpPath', ['/etc', '/var']); + await history.box.put('sshTabs', '[]'); + + final docker = await open('docker'); + await docker.box.put('containerHostdocker', 'unix:///var/run/docker.sock'); + + final stats = await open('connection_stats'); + await stats.box.put( + 'srv-1_1000', + ConnectionStat( + serverId: 'srv-1', + serverName: 'prod', + timestamp: DateTime.now(), + result: ConnectionResult.success, + durationMs: 12, + ), + ); + + final agent = await open('agent_conversation'); + await agent.box.put('active::srv-1', 'conv-1'); + + await Hive.close(); + } + + test('every box lands in the store that replaced it', () async { + await seedHive(); + await Stores.init(); + + final spi = Stores.server.fetchOneRaw('srv-1'); + expect(spi?.name, 'prod'); + expect(spi?.ssh?.ip, '10.0.0.1'); + expect(spi?.ssh?.port, 2222); + expect(spi?.tags, ['a']); + + expect(Stores.snippet.fetchOneRaw('uptime')?.script, 'w'); + expect(Stores.key.fetchOneRaw('k1')?.key, 'PRIVATE'); + + expect(Stores.setting.timeout.get(), 9); + expect(Stores.setting.recordHistory.get(), false); + expect(Stores.setting.homeTabs.get().map((e) => e.name), ['server', 'ssh']); + + expect(Stores.history.sftpGoPath.all, ['/etc', '/var']); + expect( + Stores.container.fetch('', ContainerType.docker), + 'unix:///var/run/docker.sock', + ); + expect( + Stores.connectionStats.getConnectionHistory('srv-1').single.serverName, + 'prod', + ); + expect(Stores.agentConversation.activeConversationId('srv-1'), 'conv-1'); + }); + + test('the records are readable as JSON, not as adapter bytes', () async { + await seedHive(); + await Stores.init(); + + // The point of the exercise: nothing decodes through a TypeAdapter any + // more, so what is in the row has to stand on its own. + final raw = SqliteDb.instance.select( + 'SELECT value FROM kv WHERE store = ? AND key = ?;', + ['server', 'srv-1'], + ).single['value'] as String; + final decoded = json.decode(raw) as Map; + expect(decoded['name'], 'prod'); + expect((decoded['ssh'] as Map)['ip'], '10.0.0.1'); + }); + + test('it records the current schema version', () async { + await seedHive(); + await Stores.init(); + expect(SchemaVersion.stored, SchemaVersion.current); + }); + + test('a second launch does not import again', () async { + await seedHive(); + await Stores.init(); + + // Stand in for a user edit made after the upgrade. If the import ran a + // second time it would put the old value back. + Stores.setting.timeout.put(42); + await getIt.reset(); + + await Stores.init(); + expect(Stores.setting.timeout.get(), 42); + }); + + test('the plaintext index box is deleted, the encrypted boxes are kept', + () async { + await seedHive(); + // The one box the app opened without a cipher. + final index = File(tempDir.path.joinPath('conn_stats_index.hive')); + await index.writeAsString('idx_srv-1'); + + await Stores.init(); + + expect(index.existsSync(), isFalse, reason: 'it was never encrypted'); + expect( + File(tempDir.path.joinPath('server_enc.hive')).existsSync(), + isTrue, + reason: 'kept so a bad import can be rolled back to', + ); + }); + + test('connection stats land in their table, not in kv', () async { + await seedHive(); + await Stores.init(); + + // The box held one row per attempt plus a second, unencrypted box of key + // lists. Both are one table now, so nothing of it should be left in `kv`. + final kv = SqliteDb.instance.select( + "SELECT count(*) AS n FROM kv WHERE store LIKE 'conn%';", + ).single['n']; + expect(kv, 0); + + final rows = SqliteDb.instance.select( + 'SELECT server_id FROM conn_stat;', + ); + expect(rows.single['server_id'], 'srv-1'); + }); + + test('a fresh install imports nothing and is already current', () async { + // No boxes on disk at all. + await Stores.init(); + + expect(SchemaVersion.stored, SchemaVersion.current); + expect(Stores.server.fetch(), isEmpty); + }); + + test('a box that could not be read is retried, the rest are not recopied', + () async { + await seedHive(); + + // A box that will not open. In the field this is the keychain being + // briefly unavailable at launch on a locked iOS device, which hits the + // encrypted boxes and not the plaintext ones. Here the box file is a + // directory, which `Hive.openBox` cannot read either — corrupting the + // bytes instead does not work, because Hive recovers such a box as an + // empty one rather than failing to open it. + final encPath = tempDir.path.joinPath('snippet_enc.hive'); + final intact = File(encPath).readAsBytesSync(); + File(encPath).deleteSync(); + Directory(encPath).createSync(); + addTearDown(() { + final dir = Directory(encPath); + if (dir.existsSync()) dir.deleteSync(recursive: true); + }); + // `runIfNeeded` looks for either name, so the box still counts as present. + final plain = File(tempDir.path.joinPath('snippet.hive'))..createSync(); + + await Stores.init(); + + expect(Stores.server.fetchOneRaw('srv-1')?.name, 'prod', + reason: 'a box that opened is across'); + expect(Stores.snippet.fetchOneRaw('uptime'), isNull, + reason: 'the box that did not open has nothing across'); + + // Stands in for a user edit between the two launches. The app is usable + // with the import unfinished, so copying every box again would put the old + // value back. + Stores.setting.timeout.put(42); + await getIt.reset(); + + // The box is readable again. + Directory(encPath).deleteSync(recursive: true); + plain.deleteSync(); + File(encPath).writeAsBytesSync(intact); + await Stores.init(); + + expect(Stores.snippet.fetchOneRaw('uptime')?.script, 'w', + reason: 'the unread box is retried'); + expect(Stores.setting.timeout.get(), 42, + reason: 'a box already copied is not copied a second time'); + }); + + test('a record the destination rejects does not hold its box open', () async { + await seedHive(); + // `importRow` takes a map, so a String is rejected. Seeded here rather + // than in `seedHive`, which the other tests share. + final stats = HiveStore('connection_stats'); + await stats.init(); + await stats.box.put('bad-row', 'not a record'); + await Hive.close(); + + await Stores.init(); + + expect(Stores.connectionStats.getConnectionHistory('srv-1').length, 1, + reason: 'the readable record still lands'); + + // Deliberate, and the opposite of an unopenable box: what makes a record + // fail here — an unregistered typeId, a truncated value, a shape the + // destination will not take — gives the same answer on every later launch. + // Holding the box open for it would leave the marker unwritten for good, + // so the import would re-run every launch and `conn_stats_index` would + // stay on disk in plaintext, which is the thing deleting it exists to + // avoid. + expect( + Stores.setting.get('${StoreDefaults.prefixKey}hiveImported'), + true, + reason: 'the box is done; the rejected record is not retried', + ); + }); + + test('importing does not present itself as a local edit', () async { + await seedHive(); + await Stores.init(); + + // `Stores.lastModTime` drives which side of a sync wins. Stamping each row + // as it landed would make a device that has just finished reading its own + // disk claim the newer copy of everything. + expect(Stores.lastModTime, 0); + }); +} diff --git a/test/identity_file_key_test.dart b/test/identity_file_key_test.dart index c15642667f..aaad09bc7f 100644 --- a/test/identity_file_key_test.dart +++ b/test/identity_file_key_test.dart @@ -13,10 +13,9 @@ library; import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/core/utils/server.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; @@ -68,30 +67,22 @@ void main() { }); group('migrateIdentityFilePaths', () { - late Directory tempDir; - late Box serverBox; - late Box keyBox; late ServerStore servers; late PrivateKeyStore keys; - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('identity-file-'); - Hive.init(tempDir.path); - // In memory: a real write started in a test body completes on a callback - // the fake-async zone is no longer pumping, and `close()` then blocks - serverBox = await Hive.openBox('server_test', bytes: Uint8List(0)); - keyBox = await Hive.openBox('key_test', bytes: Uint8List(0)); - servers = ServerStore.forBox(serverBox); - keys = PrivateKeyStore.forBox(keyBox); + setUp(() { + // In memory: this tree writes as it builds, and a test has no + // business leaving a database behind. + SqliteDb.openInMemory(); + servers = ServerStore.forTest(); + keys = PrivateKeyStore.forTest(); }); - tearDown(() async { - await serverBox.close(); - await keyBox.close(); - await tempDir.delete(recursive: true); - }); + tearDown(SqliteDb.close); - Spi reread(String id) => servers.get(id)!; + /// Rows are JSON now, so a plain `get` would hand back the map. This + /// is the typed read. + Spi reread(String id) => servers.fetchOneRaw(id)!; test('a path that names no stored key moves to keyPath', () { servers.put( diff --git a/test/pane_width_test.dart b/test/pane_width_test.dart index fe268acdf7..63e624aeeb 100644 --- a/test/pane_width_test.dart +++ b/test/pane_width_test.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/generated/l10n/lib_l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/provider/server/selection.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/private_key.dart'; @@ -34,22 +32,15 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box serverBox; - late Box keyBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-pane-'); - Hive.init(tempDir.path); - // In memory: this page persists tags, order and the column's own width, - // and a real write started in a `testWidgets` body never lets go of the - // box's lock. - settingBox = await Hive.openBox('setting_test', bytes: Uint8List(0)); - serverBox = await Hive.openBox('server_test', bytes: Uint8List(0)); - keyBox = await Hive.openBox('key_test', bytes: Uint8List(0)); - getIt.registerSingleton(SettingStore.forBox(settingBox)); - getIt.registerSingleton(ServerStore.forBox(serverBox)); - getIt.registerSingleton(PrivateKeyStore.forBox(keyBox)); + SqliteDb.openInMemory(); + // In memory: this tree writes as it builds, and a test has no + // business leaving a database behind. + getIt.registerSingleton(SettingStore.forTest()); + getIt.registerSingleton(ServerStore.forTest()); + getIt.registerSingleton(PrivateKeyStore.forTest()); // 0 is what `normalizeServerStatusRefreshSeconds` reads as off; its // periodic timer would otherwise outlive the tree and fail the run. Stores.setting.serverStatusUpdateInterval.put(0); @@ -57,9 +48,7 @@ void main() { tearDown(() async { await getIt.reset(); - await settingBox.close(); - await serverBox.close(); - await keyBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/sandbox_import_test.dart b/test/sandbox_import_test.dart index 254b7b5f33..78c7e3dd56 100644 --- a/test/sandbox_import_test.dart +++ b/test/sandbox_import_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:server_box/core/utils/sandbox_import.dart'; @@ -138,16 +139,20 @@ void main() { // It describes a WAL index belonging to whatever process had the database // open. Copied, it either costs a rebuild or makes sqlite refuse; the WAL // itself is what the data is in, and that does come across. - await write(src, 'box.hive'); - await write(src, 'app.db'); - await write(src, 'app.db-wal'); - await write(src, 'app.db-shm'); + const db = SqliteDb.fileName; + await write(src, db); + await write(src, '$db-wal'); + await write(src, '$db-shm'); + // Not ours, and it only looks like a sidecar. Skipping by suffix took a + // file of the user's with it. + await write(src, 'notes-shm'); expect(await import(), SandboxImportResult.imported); - expect(exists('app.db'), isTrue); - expect(exists('app.db-wal'), isTrue); - expect(exists('app.db-shm'), isFalse); + expect(exists(db), isTrue); + expect(exists('$db-wal'), isTrue); + expect(exists('$db-shm'), isFalse); + expect(exists('notes-shm'), isTrue); }); test('what the user downloaded stays where it is, and is named', () async { diff --git a/test/server_card_gesture_test.dart b/test/server_card_gesture_test.dart index 3d2283c96f..913e5d3e14 100644 --- a/test/server_card_gesture_test.dart +++ b/test/server_card_gesture_test.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/generated/l10n/lib_l10n.dart'; @@ -7,7 +6,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/private_key.dart'; import 'package:server_box/data/store/server.dart'; @@ -29,21 +27,15 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box serverBox; - late Box keyBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-card-'); - Hive.init(tempDir.path); - // In memory: this page persists tags and order, and a real write started - // in a `testWidgets` body never lets go of the box's lock. - settingBox = await Hive.openBox('setting_test', bytes: Uint8List(0)); - serverBox = await Hive.openBox('server_test', bytes: Uint8List(0)); - keyBox = await Hive.openBox('key_test', bytes: Uint8List(0)); - getIt.registerSingleton(SettingStore.forBox(settingBox)); - getIt.registerSingleton(ServerStore.forBox(serverBox)); - getIt.registerSingleton(PrivateKeyStore.forBox(keyBox)); + SqliteDb.openInMemory(); + // In memory: this tree writes as it builds, and a test has no + // business leaving a database behind. + getIt.registerSingleton(SettingStore.forTest()); + getIt.registerSingleton(ServerStore.forTest()); + getIt.registerSingleton(PrivateKeyStore.forTest()); // No auto-refresh: 0 is what `normalizeServerStatusRefreshSeconds` reads // as off, and its periodic timer would otherwise outlive the tree and // fail the run on a pending timer. @@ -52,9 +44,7 @@ void main() { tearDown(() async { await getIt.reset(); - await settingBox.close(); - await serverBox.close(); - await keyBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/server_func_btn_test.dart b/test/server_func_btn_test.dart index 9d9accbee1..65bdd9b457 100644 --- a/test/server_func_btn_test.dart +++ b/test/server_func_btn_test.dart @@ -1,7 +1,5 @@ -import 'dart:io'; - +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/setting.dart'; @@ -9,32 +7,25 @@ import 'package:server_box/data/store/setting.dart'; /// What `ServerFuncBtn.autoAddNewFuncs` does to a row the user has already /// arranged, across an upgrade. void main() { - late Directory tempDir; - late Box box; - - setUpAll(() async { - tempDir = await Directory.systemTemp.createTemp('server-box-func-btn-'); - Hive.init(tempDir.path); - box = await Hive.openBox('setting_test'); - getIt.registerSingleton(SettingStore.forBox(box)); - }); + late SettingStore setting; - setUp(() async { - await box.clear(); + setUp(() { + SqliteDb.openInMemory(); + setting = SettingStore.forTest(); + getIt.registerSingleton(setting); }); - tearDownAll(() async { + tearDown(() async { await getIt.reset(); - await box.close(); - await tempDir.delete(recursive: true); + await SqliteDb.close(); }); /// The stored row, as indices — what the setting actually holds. - List row() => (box.get('serverBtns') as List?)?.cast() ?? const []; + List row() => setting.serverFuncBtns.get(); test('adds every entry that shipped during the upgrade', () async { // A row from before systemd (1058), portForward (1340) and power (1481). - await box.put('serverBtns', [ + setting.serverFuncBtns.put([ ServerFuncBtn.terminal.index, ServerFuncBtn.files.index, ]); @@ -51,7 +42,7 @@ void main() { }); test('adds nothing for an upgrade that shipped no new entry', () async { - await box.put('serverBtns', [ServerFuncBtn.terminal.index]); + setting.serverFuncBtns.put([ServerFuncBtn.terminal.index]); ServerFuncBtn.autoAddNewFuncs(1481, 1600); @@ -63,7 +54,7 @@ void main() { // install has been running 1500, and the user took it out of the row. An // upgrade to 1600 must not put it back — and would have, when the rule was // `to >= addedVersion` alone. - await box.put('serverBtns', [ + setting.serverFuncBtns.put([ ServerFuncBtn.terminal.index, ServerFuncBtn.systemd.index, ]); @@ -74,7 +65,7 @@ void main() { }); test('an entry already in the row is not added twice', () async { - await box.put('serverBtns', [ + setting.serverFuncBtns.put([ ServerFuncBtn.power.index, ServerFuncBtn.terminal.index, ]); @@ -94,10 +85,11 @@ void main() { ServerFuncBtn.autoAddNewFuncs(0, 1600); expect( - box.get('serverBtns'), + setting.get('serverBtns'), isNull, reason: 'nothing was written, so the defaults still apply', ); + expect(row(), ServerFuncBtn.defaultIdxs); expect( ServerFuncBtn.defaultIdxs, containsAll([ diff --git a/test/setting_store_test.dart b/test/setting_store_test.dart index ba833bb383..9730a13578 100644 --- a/test/setting_store_test.dart +++ b/test/setting_store_test.dart @@ -1,70 +1,69 @@ -import 'dart:io'; - +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/store/setting.dart'; void main() { - late Directory tempDir; - late Box box; late SettingStore store; - setUpAll(() async { - tempDir = await Directory.systemTemp.createTemp('server-box-setting-test-'); - Hive.init(tempDir.path); - box = await Hive.openBox('setting_test'); - store = SettingStore.forBox(box); - }); - - setUp(() async { - await box.clear(); + setUp(() { + SqliteDb.openInMemory(); + store = SettingStore.forTest(); }); - tearDownAll(() async { - await box.close(); - await tempDir.delete(recursive: true); - }); + tearDown(SqliteDb.close); test('adds Agent to the legacy default home tabs once', () async { - await box.put('homeTabs', ['server', 'ssh', 'file', 'snippet']); + store.set('homeTabs', ['server', 'ssh', 'file', 'snippet']); await store.migrateHomeTabsAgent(); - expect(box.get('homeTabs'), ['server', 'ssh', 'file', 'snippet', 'agent']); - expect(box.get('homeTabsAgentMigrated'), isTrue); + expect(store.get('homeTabs'), [ + 'server', + 'ssh', + 'file', + 'snippet', + 'agent', + ]); + expect(store.get('homeTabsAgentMigrated'), isTrue); }); test('preserves a custom home tab configuration', () async { - await box.put('homeTabs', ['server', 'ssh']); + store.set('homeTabs', ['server', 'ssh']); await store.migrateHomeTabsAgent(); - expect(box.get('homeTabs'), ['server', 'ssh']); - expect(box.get('homeTabsAgentMigrated'), isTrue); + expect(store.get('homeTabs'), ['server', 'ssh']); + expect(store.get('homeTabsAgentMigrated'), isTrue); }); test('preserves home tabs that already contain Agent', () async { - await box.put('homeTabs', ['server', 'ssh', 'file', 'snippet', 'agent']); + store.set('homeTabs', ['server', 'ssh', 'file', 'snippet', 'agent']); await store.migrateHomeTabsAgent(); - expect(box.get('homeTabs'), ['server', 'ssh', 'file', 'snippet', 'agent']); - expect(box.get('homeTabsAgentMigrated'), isTrue); + expect(store.get('homeTabs'), [ + 'server', + 'ssh', + 'file', + 'snippet', + 'agent', + ]); + expect(store.get('homeTabsAgentMigrated'), isTrue); }); test('a second migration does not alter later custom tabs', () async { - await box.put('homeTabs', ['server', 'ssh', 'file', 'snippet']); + store.set('homeTabs', ['server', 'ssh', 'file', 'snippet']); await store.migrateHomeTabsAgent(); - await box.put('homeTabs', ['server', 'agent']); + store.set('homeTabs', ['server', 'agent']); await store.migrateHomeTabsAgent(); - expect(box.get('homeTabs'), ['server', 'agent']); - expect(box.get('homeTabsAgentMigrated'), isTrue); + expect(store.get('homeTabs'), ['server', 'agent']); + expect(store.get('homeTabsAgentMigrated'), isTrue); }); test('removes retired setting keys without touching active settings', () async { - await box.putAll({ + store.setAll({ 'moveOutServerTabFuncBtns': true, 'forceSinglePane': true, 'recordHistory': false, @@ -72,8 +71,17 @@ void main() { await store.removeRetiredKeys(); - expect(box.containsKey('moveOutServerTabFuncBtns'), isFalse); - expect(box.containsKey('forceSinglePane'), isFalse); - expect(box.get('recordHistory'), isFalse); + expect(store.get('moveOutServerTabFuncBtns'), isNull); + expect(store.get('forceSinglePane'), isNull); + expect(store.get('recordHistory'), isFalse); + }); + + test('a migration flag does not count as a user edit', () async { + await store.migrateHomeTabsAgent(); + + // The Hive version wrote these straight to the box to keep them out of + // `lastUpdateTs`. Sync compares that number, so a device that had only ever + // run a migration would otherwise claim the newer copy. + expect(store.lastUpdateTs, anyOf(isNull, isEmpty)); }); } diff --git a/test/settings_menu_test.dart b/test/settings_menu_test.dart index 50642717fe..5cf3e99aa0 100644 --- a/test/settings_menu_test.dart +++ b/test/settings_menu_test.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/generated/l10n/lib_l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/server.dart'; @@ -24,25 +22,20 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box serverBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-settings-'); - Hive.init(tempDir.path); + SqliteDb.openInMemory(); // In memory: a real write started in a `testWidgets` body never lets go of // the box's lock, and this page writes on nearly every switch. - settingBox = await Hive.openBox('setting_test', bytes: Uint8List(0)); - serverBox = await Hive.openBox('server_test', bytes: Uint8List(0)); - getIt.registerSingleton(SettingStore.forBox(settingBox)); + getIt.registerSingleton(SettingStore.forTest()); // The server order page reads it as soon as it is shown. - getIt.registerSingleton(ServerStore.forBox(serverBox)); + getIt.registerSingleton(ServerStore.forTest()); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); - await serverBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/snippet_list_test.dart b/test/snippet_list_test.dart index eda7227c63..b12fcb9d81 100644 --- a/test/snippet_list_test.dart +++ b/test/snippet_list_test.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/generated/l10n/lib_l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/store/setting.dart'; @@ -26,30 +24,19 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box snippetBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-snippet-'); - Hive.init(tempDir.path); + SqliteDb.openInMemory(); // In memory: this page persists the pane width on every drag, and a real // write started in a `testWidgets` body never lets go of the box's lock. - settingBox = await Hive.openBox( - 'setting_test', - bytes: Uint8List(0), - ); - snippetBox = await Hive.openBox( - 'snippet_test', - bytes: Uint8List(0), - ); - getIt.registerSingleton(SettingStore.forBox(settingBox)); - getIt.registerSingleton(SnippetStore.forBox(snippetBox)); + getIt.registerSingleton(SettingStore.forTest()); + getIt.registerSingleton(SnippetStore.forTest()); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); - await snippetBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/sqlite_store_test.dart b/test/sqlite_store_test.dart new file mode 100644 index 0000000000..2f2dcfbaa7 --- /dev/null +++ b/test/sqlite_store_test.dart @@ -0,0 +1,407 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqlite3/sqlite3.dart'; + +/// Covers the two halves of [SqliteStore] separately. +/// +/// The store's own behaviour runs against an in-memory database, because the +/// keyed path needs the platform vault and a test that reaches the keychain +/// cannot run on a bare CI machine. The keying itself is exercised directly +/// against a temp file with a fixed key, which is the part that has to be right +/// before any real data is written through it. +void main() { + group('sqlite3mc is what got compiled in', () { + test('the multiple-ciphers build reports itself', () { + final db = sqlite3.openInMemory(); + addTearDown(db.close); + + // Absent from a plain SQLite build, so this failing means + // `hooks.user_defines.sqlite3.source` in pubspec.yaml did not take. + final rows = db.select('SELECT sqlite3mc_version() AS v;'); + expect(rows.single['v'], isA()); + }); + }); + + group('encryption', () { + late Directory dir; + + setUp(() => dir = Directory.systemTemp.createTempSync('sbm_sqlite_test')); + tearDown(() => dir.deleteSync(recursive: true)); + + String hex(Uint8List b) => + b.map((e) => e.toRadixString(16).padLeft(2, '0')).join(); + + Uint8List randomKey() => Uint8List.fromList( + List.generate(32, (_) => Random.secure().nextInt(256)), + ); + + void key(Database db, Uint8List k) { + db.execute("PRAGMA cipher = 'chacha20';"); + db.execute('PRAGMA key = "x\'${hex(k)}\'";'); + } + + test('a raw key round-trips, and the file is not plaintext', () { + final path = '${dir.path}/keyed.db'; + final k = randomKey(); + + final db = sqlite3.open(path); + key(db, k); + db.execute('CREATE TABLE t (v TEXT);'); + db.execute("INSERT INTO t VALUES ('a-recognisable-secret');"); + db.close(); + + final reopened = sqlite3.open(path); + key(reopened, k); + expect(reopened.select('SELECT v FROM t;').single['v'], + 'a-recognisable-secret'); + reopened.close(); + + // The point of the whole exercise: neither the value nor the schema is + // readable in the file. Hive only ever encrypted values, which is what + // made `conn_stats_index.hive` legible. + final bytes = File(path).readAsBytesSync(); + expect(String.fromCharCodes(bytes), isNot(contains('a-recognisable-secret'))); + expect(String.fromCharCodes(bytes), isNot(contains('CREATE TABLE'))); + }); + + test('the wrong key does not open the file', () { + final path = '${dir.path}/keyed.db'; + + final db = sqlite3.open(path); + key(db, randomKey()); + db.execute('CREATE TABLE t (v TEXT);'); + db.close(); + + final reopened = sqlite3.open(path); + key(reopened, randomKey()); + // `PRAGMA key` itself succeeds; the first statement that reads a page is + // where it fails. `SqliteDb._open` runs exactly this for that reason. + expect( + () => reopened.select('SELECT count(*) FROM sqlite_master;'), + throwsA(isA()), + ); + reopened.close(); + }); + }); + + group('SqliteStore', () { + late SqliteStore store; + late SqliteStore other; + + setUp(() { + SqliteDb.openInMemory(); + store = SqliteStore('a'); + other = SqliteStore('b'); + }); + tearDown(SqliteDb.close); + + test('round-trips primitives, maps and lists', () { + store.set('s', 'txt'); + store.set('i', 42); + store.set('b', true); + store.set('m', {'x': 1}); + store.set('l', [1, 2, 3]); + + expect(store.get('s'), 'txt'); + expect(store.get('i'), 42); + expect(store.get('b'), true); + expect(store.get('m'), {'x': 1}); + expect(store.get('l'), [1, 2, 3]); + }); + + test('an absent key is null, not a throw', () { + expect(store.get('nope'), isNull); + }); + + test('stores with the same key do not see each other', () { + store.set('k', 'from-a'); + other.set('k', 'from-b'); + + expect(store.get('k'), 'from-a'); + expect(other.get('k'), 'from-b'); + }); + + test('keys() hides internal keys unless asked', () { + store.set('visible', 1); + // Written by `updateLastUpdateTs` on every set above. + expect(store.keys(), {'visible'}); + expect( + store.keys(includeInternalKeys: true), + containsAll(['visible', store.lastUpdateTsKey]), + ); + }); + + test('remove drops one key, clear drops the store only', () { + store.set('x', 1); + store.set('y', 2); + other.set('x', 3); + + store.remove('x'); + expect(store.get('x'), isNull); + expect(store.get('y'), 2); + + store.clear(); + expect(store.keys(), isEmpty); + expect(other.get('x'), 3); + }); + + test('clear keeps the last-update map', () { + store.set('x', 1); + final before = store.lastUpdateTs; + expect(before, isNotNull); + expect(before!['x'], isNotNull); + + store.clear(); + + // Not just non-null: `clear` used to put the map back in a shape the + // reader rejected, so the entries have to survive, not only the key. + final after = store.lastUpdateTs; + expect(after, isNotNull); + expect(after!['x'], isNotNull); + }); + + test('enums are stored by name, not index', () { + store.set('e', _Fruit.pear); + expect(store.get('e'), 'pear'); + expect( + store.get<_Fruit>('e', fromObj: (v) => _Fruit.values.byName(v as String)), + _Fruit.pear, + ); + }); + + test('an object is stored through toJson', () { + store.set('o', _Point(1, 2)); + expect(store.get('o'), {'x': 1, 'y': 2}); + }); + + test('a value that cannot be encoded fails loudly, not silently', () { + expect(store.set('bad', _NoJson()), isFalse); + expect(store.get('bad'), isNull); + }); + + test('fromObj converts when the stored shape is not T', () { + store.set('m', {'x': 1, 'y': 2}); + final p = store.get<_Point>( + 'm', + fromObj: (v) => _Point((v as Map)['x'] as int, v['y'] as int), + ); + expect(p?.x, 1); + expect(p?.y, 2); + }); + + test('setAll writes every entry', () { + expect(store.setAll({'a': 1, 'b': 2}), isTrue); + expect(store.get('a'), 1); + expect(store.get('b'), 2); + }); + + test('getAllMap returns the store contents without internal keys', () { + store.set('a', 1); + store.set('b', 'two'); + expect(store.getAllMap(), {'a': 1, 'b': 'two'}); + }); + + test('a property notifies its own listeners on write', () { + final prop = store.property('n'); + final listenable = prop.listenable(); + + var calls = 0; + void onChange() => calls++; + listenable.addListener(onChange); + addTearDown(() => listenable.removeListener(onChange)); + + prop.set(1); + expect(calls, 1); + expect(listenable.value, 1); + + // Another key on the same store must not wake this listener. + store.set('unrelated', 9); + expect(calls, 1); + + prop.remove(); + expect(calls, 2); + expect(listenable.value, isNull); + }); + + test('a default property reports its default until written', () { + final prop = store.propertyDefault('n', 7); + expect(prop.get(), 7); + prop.set(1); + expect(prop.get(), 1); + }); + + test('listProperty round-trips through JSON', () { + final prop = store.listProperty('l', defaultValue: const [1]); + expect(prop.get(), [1]); + prop.set([4, 5, 6]); + expect(prop.get(), [4, 5, 6]); + }); + + test('a removed listener stops being called', () { + final prop = store.property('n'); + final listenable = prop.listenable(); + + var calls = 0; + void onChange() => calls++; + listenable.addListener(onChange); + prop.set(1); + listenable.removeListener(onChange); + prop.set(2); + + expect(calls, 1); + }); + + test('the stored value really is JSON text', () { + store.set('m', {'x': 1}); + final raw = SqliteDb.instance.select( + 'SELECT value FROM kv WHERE store = ? AND key = ?;', + ['a', 'm'], + ).single['value'] as String; + expect(json.decode(raw), {'x': 1}); + }); + }); + + group('SqliteStore: what the review found', () { + late SqliteStore store; + + setUp(() { + SqliteDb.openInMemory(); + store = SqliteStore('r'); + }); + tearDown(SqliteDb.close); + + test('clear keeps internal keys, so a migration marker survives', () { + const marker = '${StoreDefaults.prefixKey}someMigrationDone'; + store.set(marker, true); + store.set('user-data', 1); + + store.clear(); + + expect(store.get('user-data'), isNull); + expect( + store.get(marker), + isTrue, + reason: 'a "delete all settings" that erases "this migration already ' + 'ran" makes it run again over whatever replaced the data', + ); + }); + + test('transact commits as one unit', () { + SqliteStore.transact(() { + store.set('a', 1); + store.set('b', 2); + }); + expect(store.get('a'), 1); + expect(store.get('b'), 2); + }); + + test('a throw inside transact rolls the whole thing back', () { + store.set('before', 0); + expect( + () => SqliteStore.transact(() { + store.set('a', 1); + throw StateError('interrupted'); + }), + throwsStateError, + ); + expect(store.get('a'), isNull); + expect(store.get('before'), 0); + }); + + test('transact nests, and an inner failure undoes only the inner part', () { + store.set('outer', 0); + + SqliteStore.transact(() { + store.set('outer', 1); + try { + SqliteStore.transact(() { + store.set('inner', 1); + throw StateError('inner failed'); + }); + } catch (_) { + // The outer unit decides to carry on without the inner one. + } + store.set('after', 1); + }); + + expect(store.get('outer'), 1); + expect(store.get('inner'), isNull); + expect(store.get('after'), 1); + }); + + test('a throw through both levels undoes both', () { + store.set('before', 0); + expect( + () => SqliteStore.transact(() { + store.set('outer', 1); + SqliteStore.transact(() => store.set('inner', 1)); + throw StateError('outer failed'); + }), + throwsStateError, + ); + + expect(store.get('outer'), isNull); + expect(store.get('inner'), isNull); + expect(store.get('before'), 0); + }); + + test('a nested transact leaves nothing open behind it', () { + SqliteStore.transact(() { + SqliteStore.transact(() => store.set('a', 1)); + }); + // A leaked savepoint would make this throw "cannot start a transaction + // within a transaction" — or silently join the previous one. + SqliteStore.transact(() => store.set('b', 2)); + + expect(store.get('a'), 1); + expect(store.get('b'), 2); + }); + + test('a cached statement is rebuilt when the database is replaced', () { + // Statements are kept across calls because preparing one is most of what + // a read costs. They belong to the database that made them, which + // disposes them with itself — so a store that outlives an open/close + // cycle must not reuse or re-dispose them. + store.set('a', 1); + expect(store.get('a'), 1); + + SqliteDb.close(); + SqliteDb.openInMemory(); + + expect(store.get('a'), isNull); + store.set('a', 2); + expect(store.get('a'), 2); + }); + + test('getAllMap reads every key in one pass', () { + store.set('a', 1); + store.set('m', {'x': 1}); + + expect(store.getAllMap(), { + 'a': 1, + 'm': {'x': 1}, + }); + expect( + store.getAllMap(includeInternalKeys: true).keys, + contains(store.lastUpdateTsKey), + ); + }); + }); +} + +enum _Fruit { apple, pear } + +class _Point { + const _Point(this.x, this.y); + final int x; + final int y; + Map toJson() => {'x': x, 'y': y}; +} + +class _NoJson { +} diff --git a/test/ssh_tab_restore_test.dart b/test/ssh_tab_restore_test.dart index 1051a22e1a..ae87001746 100644 --- a/test/ssh_tab_restore_test.dart +++ b/test/ssh_tab_restore_test.dart @@ -1,11 +1,10 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/core/utils/local_shell.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/terminal_source.dart'; @@ -29,38 +28,21 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; - late Box serverBox; - late Box keyBox; - late Box historyBox; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-sshtab-'); - Hive.init(tempDir.path); - // In memory, all four. This page writes its tab set on every change, and - // a real file write started inside a `testWidgets` body completes on a - // callback that zone is no longer pumping — so the box's write lock is - // never released and `close()` in `tearDown` blocks for ever, with no - // failure and no output to say which file did it. - settingBox = await Hive.openBox('setting_test', bytes: Uint8List(0)); - serverBox = await Hive.openBox('server_test', bytes: Uint8List(0)); - keyBox = await Hive.openBox('key_test', bytes: Uint8List(0)); - historyBox = await Hive.openBox( - 'history_test', - bytes: Uint8List(0), - ); - getIt.registerSingleton(SettingStore.forBox(settingBox)); - getIt.registerSingleton(ServerStore.forBox(serverBox)); - getIt.registerSingleton(PrivateKeyStore.forBox(keyBox)); - getIt.registerSingleton(HistoryStore.forBox(historyBox)); + SqliteDb.openInMemory(); + // In memory: this tree writes as it builds, and a test has no + // business leaving a database behind. + getIt.registerSingleton(SettingStore.forTest()); + getIt.registerSingleton(ServerStore.forTest()); + getIt.registerSingleton(PrivateKeyStore.forTest()); + getIt.registerSingleton(HistoryStore.forTest()); }); tearDown(() async { await getIt.reset(); - await settingBox.close(); - await serverBox.close(); - await keyBox.close(); - await historyBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/test/stores_init_test.dart b/test/stores_init_test.dart new file mode 100644 index 0000000000..9cda7335cb --- /dev/null +++ b/test/stores_init_test.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:server_box/data/res/store.dart'; +import 'package:server_box/data/store/schema.dart'; +import 'package:server_box/hive/hive_registrar.g.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// `Stores.init` against a database that is not open yet. +/// +/// Every other suite calls `SqliteDb.openInMemory()` in `setUp`, which makes +/// `SqliteStore.init` return at its `isOpen` guard — so none of them exercise +/// the path a cold launch actually takes, where the file has to be opened +/// first. That gap hid a crash on every launch. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late Directory tempDir; + + setUpAll(() async { + tempDir = await Directory.systemTemp.createTemp('sbm-stores-init-'); + // `late final`, so once per process. + Paths.doc = tempDir.path; + + // `HiveImport` runs inside `Stores.init` and opens boxes through + // `HiveStore`; registering twice throws, so this is once per process too. + Hive.init(tempDir.path); + Hive.registerAdapters(); + }); + + tearDownAll(() => tempDir.delete(recursive: true)); + + setUp(() async { + // One seeded generator reused, not a new one per byte — `Random(7)` inside + // the closure would have produced the same value 32 times. + final rng = Random(7); + FlutterSecureStorage.setMockInitialValues({ + 'hivePwd': base64UrlEncode( + Uint8List.fromList(List.generate(32, (_) => rng.nextInt(256))), + ), + }); + SharedPreferences.setMockInitialValues({}); + await PrefStore.shared.init(); + + }); + + tearDown(() async { + await getIt.reset(); + await SqliteDb.close(); + }); + + test('it opens the database before any store reads it', () async { + // The whole test. `ConnectionStatsStore.init` and + // `AgentConversationStore.init` reach `SqliteDb.instance` synchronously to + // create their tables, so they cannot be started in the same batch as the + // K-V stores that are still opening the file. + await Stores.init(); + + expect(SqliteDb.isOpen, isTrue); + expect(SqliteDb.path, tempDir.path.joinPath(SqliteDb.fileName)); + }); + + test('every store is usable once init returns', () async { + await Stores.init(); + + Stores.setting.timeout.put(11); + expect(Stores.setting.timeout.get(), 11); + + // The two table-backed stores answer only if their tables were created. + expect(Stores.connectionStats.getAllServerStats(), isEmpty); + expect(Stores.agentConversation.fetchForServer('srv'), isEmpty); + }); + + test('a second launch reopens the same file', () async { + // Deliberately not deleting the file between the two `Stores.init` calls. + await Stores.init(); + Stores.setting.timeout.put(23); + + await getIt.reset(); + await SqliteDb.close(); + + await Stores.init(); + expect(Stores.setting.timeout.get(), 23); + }); + + test('the schema version does not travel in a backup', () async { + await Stores.init(); + + // It describes this device's storage. Carried in a backup, restoring one + // taken on a device still on the previous release wrote that version back, + // and the next launch found no migration registered for it and threw a + // StateError nothing catches. + expect(Stores.setting.getAllMap().keys, isNot(contains('schemaVersion'))); + expect( + Stores.setting.getAllMap().keys.any((k) => k.contains('schemaVersion')), + isFalse, + ); + expect(SchemaVersion.stored, SchemaVersion.current); + }); + + test('clearing the settings does not make the import run again', () async { + const marker = '${StoreDefaults.prefixKey}hiveImported'; + + await Stores.init(); + expect(Stores.setting.get(marker), isTrue); + Stores.setting.timeout.put(31); + + Stores.setting.clear(); + + // The marker is internal, so `clear` leaves it — directly, not inferred + // from the schema version. Were it dropped, the next launch would copy the + // retained Hive boxes back over everything the user has since changed. + expect(Stores.setting.timeout.get(), isNot(31), reason: 'settings cleared'); + expect(Stores.setting.get(marker), isTrue); + + await getIt.reset(); + await SqliteDb.close(); + await Stores.init(); + + expect(Stores.setting.get(marker), isTrue); + expect(SchemaVersion.stored, SchemaVersion.current); + }); +} diff --git a/test/terminal_clipboard_test.dart b/test/terminal_clipboard_test.dart index c2223428fd..ef64a8b0f9 100644 --- a/test/terminal_clipboard_test.dart +++ b/test/terminal_clipboard_test.dart @@ -1,11 +1,11 @@ import 'dart:io'; +import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_ce/hive.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/terminal_session.dart'; import 'package:server_box/data/ssh/terminal_source.dart'; @@ -28,14 +28,12 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; - late Box settingBox; final clipboard = []; setUp(() async { tempDir = await Directory.systemTemp.createTemp('server-box-term-'); - Hive.init(tempDir.path); - settingBox = await Hive.openBox('setting_test'); - getIt.registerSingleton(SettingStore.forBox(settingBox)); + SqliteDb.openInMemory(); + getIt.registerSingleton(SettingStore.forTest()); clipboard.clear(); // The real channel would reach a platform that is not here. Recorded @@ -57,7 +55,7 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, null); await getIt.reset(); - await settingBox.close(); + await SqliteDb.close(); await tempDir.delete(recursive: true); }); diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index aed579f6fd..e734d00491 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -14,9 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - flutter_pty jni - sbm_ffi ) set(PLUGIN_BUNDLED_LIBRARIES)