From 7585e1245cb8bce0579e0a5228ebb04dfe8d2369 Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 04:19:49 +0800 Subject: [PATCH 1/8] feat: add OpenHarmony native assets support --- .github/workflows/ci.yml | 66 ++++++++++++- .gitignore | 1 + README.md | 7 +- README.zh-CN.md | 6 +- analysis_options.yaml | 1 + doc/ohos.zh-CN.md | 149 +++++++++++++++++++++++++++++ doc/verification.md | 3 +- doc/verification.zh-CN.md | 3 +- hook/build.dart | 101 ++++++++++++++++---- lib/src/build_support/ohos.dart | 87 +++++++++++++++++ pubspec.yaml | 6 +- src/CMakeLists.txt | 13 +++ test/ohos_build_test.dart | 114 +++++++++++++++++++++++ tool/build_ohos.py | 160 ++++++++++++++++++++++++++++++++ tool/check_native_build.dart | 20 +++- tool/create_flutter_smoke.dart | 58 ++++++++++-- tool/native_smoke.cpp | 8 +- tool/ohos/CMakeLists.txt | 37 ++++++++ tool/ohos/EchHttpDeps.cmake | 15 +++ 19 files changed, 815 insertions(+), 40 deletions(-) create mode 100644 doc/ohos.zh-CN.md create mode 100644 lib/src/build_support/ohos.dart create mode 100644 test/ohos_build_test.dart create mode 100644 tool/build_ohos.py create mode 100644 tool/ohos/CMakeLists.txt create mode 100644 tool/ohos/EchHttpDeps.cmake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f13d6c2..7055d36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: description: Platform checks to run type: choice default: all - options: [all, desktop, android, apple] + options: [all, desktop, android, apple, ohos] permissions: contents: read concurrency: @@ -88,3 +88,67 @@ jobs: flutter build ios --release --no-codesign flutter build ios --simulator --debug flutter build macos --debug + + ohos: + if: github.event_name != 'workflow_dispatch' || inputs.platform == 'all' || inputs.platform == 'ohos' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + - run: sudo apt-get update && sudo apt-get install -y cmake ninja-build git python3 + - uses: ErBWs/setup-ohos@3a251d7e29546c19a9f5fbd3aa4614f6df196910 + with: + version: 26.0.0.821 + cache: true + - run: dart pub get + - run: dart test test/ohos_build_test.dart -r expanded + - name: Cross-compile using the Flutter-OH compatibility protocol + shell: bash + run: | + for arch in arm64 x64; do + dart run tool/check_native_build.dart ohos "$arch" + done + - name: Collect native libraries and hook manifests + shell: bash + run: | + python3 - <<'PY' + import json + from pathlib import Path + import shutil + for arch in ('arm64', 'x64'): + manifest = Path(f'.dart_tool/verify/ohos-{arch}-device/output.json') + data = json.loads(manifest.read_text()) + assert data['status'] == 'success' + assets = [a['encoding'] for a in data['assets']] + library = next(a['file'] for a in assets if a['id'] == 'package:ech_http/ech_http_bindings') + destination = Path('build/ohos-native-assets') / arch + destination.mkdir(parents=True, exist_ok=True) + shutil.copyfile(library, destination / 'libech_http.so') + shutil.copyfile(manifest, destination / 'output.json') + PY + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - name: Set up the verified Flutter-OH toolchain + shell: bash + run: | + git init "$RUNNER_TEMP/flutter-ohos" + git -C "$RUNNER_TEMP/flutter-ohos" remote add origin https://gitcode.com/CPF-Flutter/flutter_flutter.git + git -C "$RUNNER_TEMP/flutter-ohos" fetch --depth=1 origin 62357a93d653bf844f730bb1f4b7cc9e2139d14e + git -C "$RUNNER_TEMP/flutter-ohos" checkout --detach FETCH_HEAD + echo "$RUNNER_TEMP/flutter-ohos/bin" >> "$GITHUB_PATH" + echo "CI=true" >> "$GITHUB_ENV" + - run: dart run tool/create_flutter_smoke.dart ohos + - name: Build a Flutter consumer HAP with the native assets + working-directory: build/flutter_smoke + run: | + flutter pub get + flutter build hap --release --no-codesign + - uses: actions/upload-artifact@v4 + with: + name: ohos-native-assets + path: | + build/ohos-native-assets/ + build/flutter_smoke/build/ohos/hap/*.hap diff --git a/.gitignore b/.gitignore index 8ef16fc..f7f49b7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ coverage/ pubspec.lock *.jpg *.log +__pycache__/ diff --git a/README.md b/README.md index 783d487..3a9f70a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # ech_http +This fork adds [OpenHarmony / HarmonyOS build support](doc/ohos.zh-CN.md) +using Flutter-OH Native Assets and pinned source builds of the native dependencies. +See that guide for toolchain requirements and verification limits. + [![Dart SDK](https://img.shields.io/badge/Dart-3.10+-0175C2.svg?logo=dart)](https://dart.dev) [![Flutter](https://img.shields.io/badge/Flutter-3.38+-02569B.svg?logo=flutter)](https://flutter.dev) [![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-blue.svg)](#supported-platforms-and-toolchain) @@ -73,9 +77,10 @@ prompt cleanup. | **macOS** | `x64`, `arm64` | macOS host, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (device), `arm64`/`x64` (simulator) | macOS host, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (or Flutter bundled NDK), CMake, Ninja | Android API level 21+ (5.0 Lollipop) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, Python 3.12+, Git, Go, CMake, Ninja; Flutter-OH Native Assets for HAPs | See [OHOS validation](doc/ohos.zh-CN.md) | > [!NOTE] -> Web (Browsers) and native HarmonyOS are not currently supported. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. +> Web (Browsers) is not supported. OHOS builds its pinned dependencies from source on the first build; the other platforms use prebuilt SDKs. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 0447ca4..4e4c264 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,6 +8,9 @@ [English](README.md) +本分支新增 [OpenHarmony / HarmonyOS 构建支持](doc/ohos.zh-CN.md), +通过 Flutter-OH Native Assets 从固定版本源码构建原生依赖,详见接入与验证说明。 + `ech_http` 是专为 Dart 和 Flutter 打造的企业级 HTTP 客户端,深度集成了 **TLS 密文客户端问候(Encrypted Client Hello, ECH)**、HTTP CONNECT 代理、自定义 PKI 信任根及流式数据传输功能。完全兼容官方 `package:http.Client` 接口规范。 底层核心由基于 **libcurl** 和 **BoringSSL** 构建的 C++17 原生引擎驱动,通过 Dart 现代构建钩子(Build Hooks / `code_assets`)实现跨平台自动打包。**在运行时,无需启动任何外部后台进程、无需部署本地监听代理、无需注册 Flutter 平台插件通道,也无需应用层手动管理动态链接库加载。** @@ -70,9 +73,10 @@ flowchart TD | **macOS** | `x64`, `arm64` | macOS 系统, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (真机), `arm64`/`x64` (模拟器) | macOS 系统, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (或 Flutter 自带 NDK), CMake, Ninja | Android API Level 21+ (5.0 Lollipop) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK、Python 3.12+、Git、Go、CMake、Ninja;HAP 需要 Flutter-OH Native Assets | 见[鸿蒙验证说明](doc/ohos.zh-CN.md) | > [!NOTE] -> 当前暂不支持浏览器 Web 环境及原生鸿蒙(HarmonyOS)。Android 原生库使用静态 libc++ 链接,并满足 Android 15 强制要求的 16 KiB ELF 页面对齐。 +> 当前不支持浏览器 Web 环境。鸿蒙首次构建从固定版本源码编译依赖,其他平台使用预编译 SDK。Android 原生库使用静态 libc++ 链接,并满足 Android 15 强制要求的 16 KiB ELF 页面对齐。 --- diff --git a/analysis_options.yaml b/analysis_options.yaml index 25bcbab..74d28bb 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,4 @@ include: package:lints/recommended.yaml analyzer: exclude: - .dart_tool/** + - build/** diff --git a/doc/ohos.zh-CN.md b/doc/ohos.zh-CN.md new file mode 100644 index 0000000..4de3948 --- /dev/null +++ b/doc/ohos.zh-CN.md @@ -0,0 +1,149 @@ +# OpenHarmony / HarmonyOS 原生适配 + +本分支支持通过 Flutter-OH 的 Native Assets 构建和打包 `ech_http`。 +保留现有 `EchClient`、ECH、证书校验、代理、gzip 和流式响应 API。 +支持 `arm64`(手机、平板)和 `x64`;各架构的验证结果见下文。 + +## 构建要求 + +- Dart >=3.10、Python >=3.12、Git、CMake >=3.22、Ninja、Go(编译 BoringSSL 所需)。 +- OpenHarmony native SDK,包含 `build/cmake/ohos.toolchain.cmake`。 + 可将 `OHOS_NDK_HOME` 指向 `native` 本身或其父目录;也支持 HarmonyOS + 命令行工具的 `HOS_SDK_HOME/default/openharmony/native` 布局。 +- 应用构建需要已支持 OHOS Native Assets 的 Flutter-OH。 + 本次核对的 CPF-Flutter `oh-3.41.9-release` 提交 + `62357a93d653bf844f730bb1f4b7cc9e2139d14e` 已包含该功能。 + 仅有旧版 FFI 插件打包支持的 Flutter-OH 不满足要求。 + +本分支允许使用 `code_assets` / `hooks` 的兼容版本范围,解决 Flutter-OH +3.41 固定 `meta 1.17.0` 与 `hooks 2.2` 的依赖冲突。实际测试应用解析为 +`code_assets 1.2.1`、`hooks 2.0.2`,无需覆盖 Flutter SDK 的 `meta` 版本。 + +Flutter-OH 当前向第三方 hook 传递 `target_os=linux`,同时提供 OHOS 的 +`native/llvm/bin/clang`。构建钩子根据编译器所属 SDK 识别鸿蒙目标,使用 +OHOS toolchain、sysroot 和架构编译,**不会使用 Linux 预编译依赖**。 +将来直接传递 `target_os=ohos` 的工具链也可以识别。 + +## 应用接入 + +在 Flutter-OH 应用的 `pubspec.yaml` 中使用本分支,例如本地依赖: + +```yaml +dependencies: + ech_http: + path: /absolute/path/to/ech_http +``` + +使用 Flutter-OH 运行 `flutter pub get` 和 `flutter build hap`。 +正常情况下自动从 Flutter 提供的编译器路径定位 native SDK。 +若工具链没有提供该路径,可在**应用**的 `pubspec.yaml` 中明确配置: + +```yaml +hooks: + user_defines: + ech_http: + target: ohos + ohos_sdk: /absolute/path/to/openharmony/native + # 可选:跨构建复用已下载的固定版本源码。 + source_cache: /absolute/path/to/ech-http-sources +``` + +首次构建会从包内 `dependencies.json` 指定的版本下载和编译 BoringSSL、 +curl、zlib。curl/zlib 校验 SHA-256,BoringSSL 校验 Git 提交。 +随后 Ninja 增量复用编译结果。无需另外创建或发布预编译依赖仓库。 +OHOS 使用静态 C++ 运行库,并隐藏其符号;HAP 不必额外打包 +`libc++_shared.so`,也不会把 TLS 库的符号暴露给其他播放器插件。 + +## 独立编译与检查 + +不必先创建 Flutter 应用即可检查交叉编译和 Native Assets 输出: + +```sh +export OHOS_NDK_HOME=/absolute/path/to/openharmony/native +dart pub get +dart run tool/check_native_build.dart ohos arm64 +``` + +该检查使用与 Flutter-OH 一致的 Linux 兼容输入和 OHOS 编译器。 +清单位于 `.dart_tool/verify/ohos-arm64-device/output.json`,包含 +`package:ech_http/ech_http_bindings` 对应的鸿蒙 `.so`。 + +也可单独生成库与原生请求测试程序: + +```sh +python3 tool/build_ohos.py \ + --native-sdk "$OHOS_NDK_HOME" \ + --dart-sdk /absolute/path/to/dart-sdk \ + --arch arm64 +``` + +产物位于 `build/ohos/arm64/bridge/out/`。`--arch x64` 使用独立的 +`build/ohos/x64/` 目录。已有 Flutter-OH 的构建会自动运行相同的依赖编译步骤。 + +原生测试程序参数与现有工具一致: + +```sh +./ech_http_smoke URL HTTP代理或空字符串 ECHConfigList或空字符串 目标IP或空字符串 +``` + +ECH 配置非空时,测试要求响应为 HTTP 200 且报告 ECH 被服务器接受。 +HarmonyOS 商用设备可能拒绝直接执行未签名 ELF;这种情况下应使用官方 +调试签名,或将库放入正常签名的测试 HAP。普通文件执行权限并不能替代代码签名。 + +## Flutter 真机测试包 + +使用 Flutter-OH 创建独立的 ECH 测试应用: + +```sh +dart run tool/create_flutter_smoke.dart ohos +cd build/flutter_smoke +flutter pub get +flutter build hap --debug --no-codesign +``` + +应用包名为 `com.example.ech_http_smoke`,支持手机、平板。 +启动后通过 DoH 获取 Cloudflare ECH 配置,发起 HTTPS 请求,并显示 +HTTP 状态、`ECH accepted` 和后端版本。使用正常的华为调试签名后安装。 +成功标准是 HTTP 200、`ECH accepted: true`,不能只根据页面打开判断成功。 + +包路径为 `build/flutter_smoke/build/ohos/hap/entry-default-unsigned.hap` +(相对于本仓库根目录)。当前 Flutter-OH 的 macOS Release AOT 编译器 +为 Intel 架构;Apple Silicon 上的 Release 构建需要 Rosetta。 +Debug 测试包不依赖这个 AOT 编译器。 + +## 验证范围 + +2026-10-17 本地检查使用 macOS arm64 构建机、Dart 3.13.5、OpenHarmony +native SDK 6.0.0.47(API 20)。Flutter HAP 使用 Flutter-OH +3.41.9+ohos-1.0.2、其内置 Dart 3.11.5 和 HarmonyOS SDK 26.0.0.105。 +连接的 MatePad mini 为 HarmonyOS API 26。 + +| 检查 | 结果 | +| --- | --- | +| arm64 原生库及测试程序交叉编译 | 通过 | +| x64 原生库及测试程序交叉编译 | 通过;未连接 x64 设备 | +| Flutter-OH 兼容输入的构建钩子 | 通过,生成 `package:ech_http/ech_http_bindings` 清单 | +| ELF 动态依赖检查 | 仅依赖 `libc.so`,没有额外的 TLS、zlib、C++ 动态库 | +| macOS Dart 单元和原生回归测试 | 通过;网络 live 测试需单独启用 | +| Flutter-OH 3.41.9+ohos-1.0.2 应用依赖解析 | 通过,兼容 Flutter 固定的 `meta 1.17.0` | +| Flutter 测试应用静态分析 | 通过 | +| Debug HAP 构建及库打包 | 通过,包内包含 arm64 `libech_http.so` 与 Native Assets 映射 | +| 本机 Release HAP 构建 | 原生库与 Dart 编译通过;AOT 编译器需要本机尚未安装的 Rosetta | +| MatePad mini 上执行裸 ELF | 系统返回 `Permission denied`,未执行到请求代码 | +| 已签名 Flutter Debug HAP 真机运行 | 通过,Native Assets 中的 arm64 库正常加载并解析 FFI 符号 | +| MatePad mini HTTPS / ECH 请求 | 通过,DoH 获取 ECH 配置后返回 HTTP 200、`ECH accepted: true`,服务端返回 `sni=encrypted`、`tls=TLSv1.3` | + +真机日志中的关键结果如下(来自正常签名安装的 Flutter HAP): + +```text +ECH_HTTP_SMOKE: libcurl/8.22.0 BoringSSL zlib/1.3.2 +HTTP 200 +ECH accepted: true +tls=TLSv1.3 +sni=encrypted +``` + +本地验证记录保存在 `build/ohos-runtime-smoke.log`。本次已验证 Flutter +Native Assets 集成和公网 HTTPS/ECH;gzip、代理、取消请求、isolate 退出 +等行为已覆盖桌面回归测试,尚未在鸿蒙真机上逐项复测。 +x64 仅完成交叉编译和构建钩子检查,Release HAP 及远程 CI 尚未运行验证。 diff --git a/doc/verification.md b/doc/verification.md index 58c1910..ccb722d 100644 --- a/doc/verification.md +++ b/doc/verification.md @@ -42,7 +42,8 @@ We distinguish between three tiers of verification: | **Android** | `arm64-v8a` | **Tier 1** | Physical device run (API 35), live ECH handshake, authenticated retry, connection reuse | Non-standard custom Android ROMs | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK packaging for both ABIs | Runtime execution on physical 32-bit ARM / x86_64 devices | | **Android** | `x86` | **Tier 2** | NDK bridge build and link check in CI | 32-bit x86 Android runtime execution | -| **Web / HarmonyOS** | All | **Tier 3** | Not supported by this native C++ FFI architecture | N/A | +| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | Both architectures cross-compile through the build hook; a signed arm64 Flutter Debug HAP returns HTTP 200 with ECH accepted on MatePad mini | See the [OHOS guide](ohos.zh-CN.md) for runtime scope; full suite not repeated on OHOS, no x64 device attached | +| **Web** | All | **Tier 3** | Not supported by this native C++ FFI architecture | N/A | --- diff --git a/doc/verification.zh-CN.md b/doc/verification.zh-CN.md index e6b7259..695f8a4 100644 --- a/doc/verification.zh-CN.md +++ b/doc/verification.zh-CN.md @@ -37,7 +37,8 @@ Flutter Android APK、iOS 未签名 Release 应用、iOS 模拟器 Debug 应用 | **Android** | `arm64-v8a` | **Tier 1** | 真实设备运行(API 35),公网 ECH 握手、认证重试、连接复用均通过 | 各类深度定制的非标准第三方 Android ROM | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK 多 ABI 打包测试通过 | 32 位 ARM / x86_64 物理机运行时执行 | | **Android** | `x86` | **Tier 2** | CI 中 NDK 桥接构建与链接核验通过 | 32 位 x86 Android 实体机运行时执行 | -| **Web / 原生鸿蒙** | 全部架构 | **Tier 3** | C++ FFI 原生引擎架构暂不支持 Web 与 HarmonyOS | 不适用 | +| **OpenHarmony / HarmonyOS** | arm64、x64 | **Tier 2** | 两种架构交叉编译与构建钩子通过;arm64 签名 Flutter Debug HAP 在 MatePad mini 返回 HTTP 200、ECH 被接受 | 真机验证范围见[鸿蒙说明](ohos.zh-CN.md);完整功能套件未在鸿蒙复测,未连接 x64 设备 | +| **Web** | 全部架构 | **Tier 3** | C++ FFI 原生引擎架构不支持 Web | 不适用 | --- diff --git a/hook/build.dart b/hook/build.dart index 2e70e4b..960b735 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -8,20 +8,29 @@ import 'package:hooks/hooks.dart'; import 'package:path/path.dart' as p; import 'package:ech_http/src/build_support/prebuilt.dart'; +import 'package:ech_http/src/build_support/ohos.dart'; Future main(List args) async { await build(args, (input, output) async { if (!input.config.buildCodeAssets) return; final code = input.config.code; - if (![ - OS.windows, - OS.linux, - OS.macOS, - OS.iOS, - OS.android, - ].contains(code.targetOS)) { + final ohos = OhosBuildConfig.detect( + targetOS: code.targetOS, + architecture: code.targetArchitecture, + compiler: code.cCompiler?.compiler, + sdkPath: input.userDefines.path('ohos_sdk')?.toFilePath(), + explicitlyOhos: input.userDefines['target'] == 'ohos', + ); + if (ohos == null && + ![ + OS.windows, + OS.linux, + OS.macOS, + OS.iOS, + OS.android, + ].contains(code.targetOS)) { throw UnsupportedError( - 'ech_http supports Android, iOS, Linux, macOS, and Windows', + 'ech_http supports Android, iOS, Linux, macOS, Windows, and OHOS', ); } final root = input.packageRoot.toFilePath(); @@ -46,21 +55,59 @@ Future main(List args) async { output.dependencies.addAll([ Uri.file(manifestPath), Uri.file(p.join(root, 'lib', 'src', 'build_support', 'prebuilt.dart')), + Uri.file(p.join(root, 'lib', 'src', 'build_support', 'ohos.dart')), ]); await for (final file in Directory( p.join(root, 'src'), ).list(recursive: true)) { if (file is File) output.dependencies.add(file.uri); } - final target = dependencyTarget( - code.targetOS, - code.targetArchitecture, - iosSdk: code.targetOS == OS.iOS ? code.iOS.targetSdk : null, - ); - final dependency = PrebuiltDependency.fromManifest(manifest, target); - final sdk = await preparePrebuilt(dependency, cache); + final target = ohos == null + ? dependencyTarget( + code.targetOS, + code.targetArchitecture, + iosSdk: code.targetOS == OS.iOS ? code.iOS.targetSdk : null, + ) + : 'ohos-${code.targetArchitecture.name}'; + final String sdk; + if (ohos == null) { + final dependency = PrebuiltDependency.fromManifest(manifest, target); + sdk = await preparePrebuilt(dependency, cache); + } else { + final python = + _onPath('python3', Platform.environment) ?? + _onPath('python', Platform.environment); + if (python == null) { + throw StateError('OHOS source builds require Python >=3.12'); + } + final builder = p.join(root, 'tool', 'build_ohos.py'); + output.dependencies.addAll([ + Uri.file(builder), + Uri.file(p.join(root, 'tool', 'ohos', 'CMakeLists.txt')), + Uri.file(p.join(root, 'tool', 'ohos', 'EchHttpDeps.cmake')), + Uri.file(p.join(ohos.nativeSdk, 'oh-uni-package.json')), + ]); + final dependencyBuild = p.join(shared, target); + stderr.writeln( + 'ech_http: building pinned OHOS dependencies (cached after the first build)', + ); + await runner.run(python, [ + builder, + '--deps-only', + '--native-sdk', + ohos.nativeSdk, + '--arch', + code.targetArchitecture.name, + '--output', + dependencyBuild, + '--source-cache', + input.userDefines.path('source_cache')?.toFilePath() ?? + p.join(dependencyBuild, 'sources'), + ]); + sdk = p.join(dependencyBuild, 'sdk'); + } - final tools = await _toolchain(code, runner); + final tools = await _toolchain(code, runner, ohos: ohos); runner.environment = tools.environment; final common = [ '-G', @@ -82,9 +129,7 @@ Future main(List args) async { output.dependencies.add(Uri.file(p.join(dartInclude, header))); } final nativeBuild = p.join(out, 'native'); - stderr.writeln( - 'ech_http: compiling C++ bridge with prebuilt $target dependencies', - ); + stderr.writeln('ech_http: compiling C++ bridge with $target dependencies'); await runner.run(tools.cmake, [ '-S', p.join(root, 'src'), @@ -129,9 +174,25 @@ final class _Tools { final List arguments; } -Future<_Tools> _toolchain(CodeConfig code, _Runner runner) async { +Future<_Tools> _toolchain( + CodeConfig code, + _Runner runner, { + OhosBuildConfig? ohos, +}) async { var environment = Map.of(Platform.environment); final arguments = []; + if (ohos != null) { + final bin = p.join(ohos.nativeSdk, 'build-tools', 'cmake', 'bin'); + final suffix = Platform.isWindows ? '.exe' : ''; + final cmake = + _onPath('cmake', environment) ?? _existing(p.join(bin, 'cmake$suffix')); + final ninja = + _onPath('ninja', environment) ?? _existing(p.join(bin, 'ninja$suffix')); + if (cmake == null || ninja == null) { + throw StateError('OHOS builds require CMake and Ninja'); + } + return _Tools(cmake, ninja, environment, ohos.cmakeArguments); + } if (code.targetOS == OS.windows && !Platform.isWindows) { throw UnsupportedError('Windows targets require a Windows host with MSVC'); } diff --git a/lib/src/build_support/ohos.dart b/lib/src/build_support/ohos.dart new file mode 100644 index 0000000..be417b3 --- /dev/null +++ b/lib/src/build_support/ohos.dart @@ -0,0 +1,87 @@ +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:path/path.dart' as p; + +/// Hosted code_assets does not know OHOS yet. Flutter-OH currently sends +/// target_os=linux with its OHOS compiler. Detect the SDK, not the build host. +final class OhosBuildConfig { + OhosBuildConfig._(this.nativeSdk, this.architecture); + + final String nativeSdk; + final Architecture architecture; + + String get abi => switch (architecture) { + Architecture.arm64 => 'arm64-v8a', + Architecture.x64 => 'x86_64', + _ => throw UnsupportedError('ech_http OHOS supports arm64 and x64'), + }; + + List get cmakeArguments => [ + '-DCMAKE_TOOLCHAIN_FILE=${p.join(nativeSdk, 'build', 'cmake', 'ohos.toolchain.cmake')}', + '-DOHOS_ARCH=$abi', + '-DOHOS_PLATFORM=OHOS', + '-DOHOS_STL=c++_static', + ]; + + static OhosBuildConfig? detect({ + required OS targetOS, + required Architecture architecture, + Uri? compiler, + String? sdkPath, + bool explicitlyOhos = false, + Map? environment, + }) { + if (explicitlyOhos && targetOS != OS.linux && targetOS.name != 'ohos') { + throw UnsupportedError( + 'An explicit OHOS target requires Flutter-OH, not $targetOS. ' + 'Apply the OHOS user_defines only to OHOS builds.', + ); + } + final compilerSdk = compiler == null + ? null + : _findNativeSdk( + p.dirname(p.dirname(p.dirname(compiler.toFilePath()))), + ); + final ohos = + explicitlyOhos || + targetOS.name == 'ohos' || + (targetOS == OS.linux && compilerSdk != null); + if (!ohos) return null; + if (architecture != Architecture.arm64 && + architecture != Architecture.x64) { + throw UnsupportedError('ech_http OHOS supports arm64 and x64'); + } + final env = environment ?? Platform.environment; + final candidates = [ + ?sdkPath, + ?compilerSdk, + if (env['OHOS_NDK_HOME'] != null) env['OHOS_NDK_HOME']!, + if (env['HOS_SDK_HOME'] != null) env['HOS_SDK_HOME']!, + ]; + for (final candidate in candidates) { + final sdk = _findNativeSdk(candidate); + if (sdk != null) return OhosBuildConfig._(sdk, architecture); + } + throw StateError( + 'OpenHarmony native SDK not found. Set OHOS_NDK_HOME or ' + 'hooks.user_defines.ech_http.ohos_sdk to the native SDK directory.', + ); + } + + static String? _findNativeSdk(String root) { + for (final candidate in [ + root, + p.join(root, 'native'), + p.join(root, 'openharmony', 'native'), + p.join(root, 'default', 'openharmony', 'native'), + ]) { + if (File( + p.join(candidate, 'build', 'cmake', 'ohos.toolchain.cmake'), + ).existsSync()) { + return p.normalize(p.absolute(candidate)); + } + } + return null; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 56031e9..d9a22be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,10 +22,12 @@ topics: - networking dependencies: archive: ^4.0.9 - code_assets: ^2.1.0 + # Flutter-OH 3.41 pins meta 1.17; hooks 2.2 requires meta 1.19. + # The build hook uses APIs available in both protocol package generations. + code_assets: '>=1.2.1 <3.0.0' crypto: ^3.0.6 ffi: ^2.1.4 - hooks: ^2.2.0 + hooks: '>=2.0.2 <3.0.0' http: ^1.6.0 path: ^1.9.1 dev_dependencies: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 588c936..1f963a4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,20 @@ if(ANDROID) elseif(UNIX AND NOT APPLE) target_link_options(ech_http PRIVATE "-Wl,--exclude-libs,ALL") endif() +if(CMAKE_SYSTEM_NAME STREQUAL "OHOS") + # The OHOS SDK adds -ggdb even to Release builds. Do not ship that debug data + # or export the statically linked TLS/C++ runtime into other native plugins. + target_link_options(ech_http PRIVATE "-Wl,--exclude-libs,ALL" "-Wl,--strip-debug") +endif() if(APPLE) set_target_properties(ech_http PROPERTIES INSTALL_NAME_DIR "@rpath" BUILD_WITH_INSTALL_NAME_DIR ON) endif() set_target_properties(ech_http PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/out" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/out") + +option(EH_BUILD_SMOKE "Build the standalone native request smoke test" OFF) +if(EH_BUILD_SMOKE) + add_executable(ech_http_smoke ../tool/native_smoke.cpp) + target_include_directories(ech_http_smoke PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" "${EH_DART_INCLUDE}") + target_link_libraries(ech_http_smoke PRIVATE ech_http Threads::Threads) + set_target_properties(ech_http_smoke PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/out") +endif() diff --git a/test/ohos_build_test.dart b/test/ohos_build_test.dart new file mode 100644 index 0000000..e9c80d2 --- /dev/null +++ b/test/ohos_build_test.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:ech_http/src/build_support/ohos.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + late Directory root; + late Directory native; + + setUp(() async { + root = await Directory.systemTemp.createTemp('ech-ohos-sdk-'); + native = Directory(p.join(root.path, 'default', 'openharmony', 'native')); + final toolchain = File( + p.join(native.path, 'build', 'cmake', 'ohos.toolchain.cmake'), + ); + await toolchain.parent.create(recursive: true); + await toolchain.writeAsString('# SDK fixture'); + }); + tearDown(() async => root.delete(recursive: true)); + + test( + 'recognizes Flutter-OH linux compatibility input by its compiler SDK', + () { + final config = OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + compiler: File(p.join(native.path, 'llvm', 'bin', 'clang')).uri, + environment: {}, + )!; + expect(config.nativeSdk, native.path); + expect(config.abi, 'arm64-v8a'); + expect(config.cmakeArguments, contains('-DOHOS_STL=c++_static')); + expect(config.cmakeArguments, contains('-DOHOS_ARCH=arm64-v8a')); + }, + ); + + test('normal Linux builds stay Linux when an OHOS SDK is installed', () { + expect( + OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + compiler: File('/usr/bin/clang').uri, + environment: {'OHOS_NDK_HOME': native.path}, + ), + isNull, + ); + }); + + test('accepts an explicit OHOS target and HOS_SDK_HOME layout', () { + final config = OhosBuildConfig.detect( + targetOS: OS.fromString('ohos'), + architecture: Architecture.x64, + environment: {'HOS_SDK_HOME': root.path}, + )!; + expect(config.nativeSdk, native.path); + expect(config.abi, 'x86_64'); + }); + + test( + 'supports explicit source-build configuration with a native SDK path', + () { + final config = OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + explicitlyOhos: true, + sdkPath: native.path, + environment: {}, + )!; + expect(config.nativeSdk, native.path); + }, + ); + + test('missing SDK fails with actionable configuration guidance', () { + expect( + () => OhosBuildConfig.detect( + targetOS: OS.fromString('ohos'), + architecture: Architecture.arm64, + environment: {}, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('OHOS_NDK_HOME'), + ), + ), + ); + }); + + test('unsupported OHOS architecture fails before compiling dependencies', () { + expect( + () => OhosBuildConfig.detect( + targetOS: OS.fromString('ohos'), + architecture: Architecture.arm, + sdkPath: native.path, + ), + throwsUnsupportedError, + ); + }); + + test('explicit OHOS override cannot mislabel a macOS asset', () { + expect( + () => OhosBuildConfig.detect( + targetOS: OS.macOS, + architecture: Architecture.arm64, + explicitlyOhos: true, + sdkPath: native.path, + ), + throwsUnsupportedError, + ); + }); +} diff --git a/tool/build_ohos.py b/tool/build_ohos.py new file mode 100644 index 0000000..6034e07 --- /dev/null +++ b/tool/build_ohos.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Cross-compile ech_http with pinned BoringSSL, curl and zlib for OpenHarmony. + +Requires Python 3.12+, Git, CMake, Ninja, and an OpenHarmony native SDK. +No Android binaries or host TLS libraries are used. Downloads are cached. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tarfile +import urllib.request + +ROOT = Path(__file__).resolve().parents[1] +ARCHITECTURES = {'arm64': 'arm64-v8a', 'x64': 'x86_64'} + + +def run(args): + print('+', ' '.join(map(str, args)), flush=True) + subprocess.run(list(map(str, args)), check=True) + + +def digest(path): + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +def native_sdk(path): + root = Path(path).expanduser().resolve() + for candidate in [root, root / 'native', root / 'openharmony/native', + root / 'default/openharmony/native']: + if (candidate / 'build/cmake/ohos.toolchain.cmake').is_file(): + return candidate + raise ValueError(f'OpenHarmony native SDK not found under {root}') + + +def download_source(pin, cache, name): + archive = cache / Path(pin['url']).name + if not archive.is_file() or digest(archive) != pin['sha256']: + partial = archive.with_suffix(archive.suffix + '.part') + try: + with urllib.request.urlopen(pin['url'], timeout=60) as source, partial.open('wb') as output: + shutil.copyfileobj(source, output) + if digest(partial) != pin['sha256']: + raise RuntimeError(f'{name} source SHA-256 mismatch') + partial.replace(archive) + finally: + partial.unlink(missing_ok=True) + directory = cache / f"{name}-{pin['version']}" + if not directory.is_dir(): + with tarfile.open(archive) as tar: + tar.extractall(cache, filter='data') + return directory + + +def sources(manifest, cache): + cache.mkdir(parents=True, exist_ok=True) + revision = manifest['boringssl']['revision'] + boring = cache / f'boringssl-{revision}' + if not (boring / '.git').is_dir(): + run(['git', 'init', boring]) + run(['git', '-C', boring, 'remote', 'add', 'origin', manifest['boringssl']['repository']]) + if not (boring / 'CMakeLists.txt').is_file(): + run(['git', '-C', boring, 'fetch', '--depth=1', 'origin', revision]) + run(['git', '-C', boring, 'checkout', '--detach', revision]) + actual = subprocess.check_output(['git', '-C', str(boring), 'rev-parse', 'HEAD'], text=True).strip() + if actual != revision: + raise RuntimeError('BoringSSL source revision mismatch') + return boring, download_source(manifest['curl'], cache, 'curl'), download_source(manifest['zlib'], cache, 'zlib') + + +def main(): + if sys.version_info < (3, 12): + raise SystemExit('OpenHarmony source builds require Python >=3.12') + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--native-sdk', default=os.environ.get('OHOS_NDK_HOME') or os.environ.get('HOS_SDK_HOME')) + parser.add_argument('--dart-sdk', type=Path) + parser.add_argument('--arch', choices=ARCHITECTURES, default='arm64') + parser.add_argument('--output', type=Path) + parser.add_argument('--source-cache', type=Path, default=ROOT / 'build/ohos/sources') + parser.add_argument('--deps-only', action='store_true') + options = parser.parse_args() + if not options.native_sdk: + parser.error('Set OHOS_NDK_HOME or pass --native-sdk pointing to an OpenHarmony SDK') + if not options.deps_only and (options.dart_sdk is None or not (options.dart_sdk / 'include/dart_native_api.h').is_file()): + parser.error('--dart-sdk must point to a Dart SDK containing include/dart_native_api.h') + sdk = native_sdk(options.native_sdk) + work = (options.output or ROOT / 'build/ohos' / options.arch).resolve() + work.mkdir(parents=True, exist_ok=True) + suffix = '.exe' if os.name == 'nt' else '' + cmake = shutil.which('cmake') or str(sdk / f'build-tools/cmake/bin/cmake{suffix}') + ninja = shutil.which('ninja') or str(sdk / f'build-tools/cmake/bin/ninja{suffix}') + if not Path(cmake).is_file() or not Path(ninja).is_file(): + parser.error('Install CMake >=3.22 and Ninja, or add the native SDK build-tools/cmake/bin to PATH') + common = ['-G', 'Ninja', f'-DCMAKE_MAKE_PROGRAM={ninja}', '-DCMAKE_BUILD_TYPE=Release', + f'-DCMAKE_TOOLCHAIN_FILE={sdk}/build/cmake/ohos.toolchain.cmake', + f'-DOHOS_ARCH={ARCHITECTURES[options.arch]}', '-DOHOS_PLATFORM=OHOS', + '-DOHOS_STL=c++_static', '-DCMAKE_POSITION_INDEPENDENT_CODE=ON', + '-DCMAKE_C_VISIBILITY_PRESET=hidden', '-DCMAKE_CXX_VISIBILITY_PRESET=hidden', + # The SDK adds --gcc-toolchain even for libc++; BoringSSL's + # -Werror must not reject this SDK-supplied, unused argument. + '-DCMAKE_C_FLAGS=-Wno-unused-command-line-argument', + '-DCMAKE_CXX_FLAGS=-Wno-unused-command-line-argument'] + manifest = json.loads((ROOT / 'lib/src/build_support/dependencies.json').read_text()) + boring, curl, zlib = sources(manifest, options.source_cache.resolve()) + parallel = str(min(os.cpu_count() or 2, 8)) + boring_build = work / 'boringssl' + run([cmake, '-S', boring, '-B', boring_build, *common, '-DBUILD_SHARED_LIBS=OFF', + '-DBUILD_TESTING=OFF', '-DOPENSSL_NO_ASM=ON', '-DOPENSSL_SMALL=ON']) + run([cmake, '--build', boring_build, '--target', 'ssl', 'crypto', '--parallel', parallel]) + zlib_build = work / 'zlib' + run([cmake, '-S', zlib, '-B', zlib_build, *common, '-DZLIB_BUILD_SHARED=OFF', + '-DZLIB_BUILD_STATIC=ON', '-DZLIB_BUILD_TESTING=OFF']) + run([cmake, '--build', zlib_build, '--target', 'zlibstatic', '--parallel', parallel]) + includes = work / 'zlib-include' + includes.mkdir(exist_ok=True) + shutil.copyfile(zlib / 'zlib.h', includes / 'zlib.h') + shutil.copyfile(zlib_build / 'zconf.h', includes / 'zconf.h') + curl_build = work / 'curl' + run([cmake, '-S', ROOT / 'tool/ohos', '-B', curl_build, *common, + f'-DEH_CURL_SOURCE={curl}', f'-DEH_BORINGSSL_SOURCE={boring}', + f'-DEH_BORINGSSL_BUILD={boring_build}', f'-DZLIB_LIBRARY={zlib_build}/libz.a', + f'-DZLIB_INCLUDE_DIR={includes}']) + run([cmake, '--build', curl_build, '--parallel', parallel]) + if 'HAVE_SSL_SET1_ECH_CONFIG_LIST:INTERNAL=1' not in (curl_build / 'CMakeCache.txt').read_text(): + raise RuntimeError('libcurl was built without BoringSSL ECH support') + if '#define HAVE_LIBZ 1' not in (curl_build / 'curl/lib/curl_config.h').read_text(): + raise RuntimeError('libcurl was built without zlib support') + stage = work / 'sdk' + (stage / 'lib').mkdir(parents=True, exist_ok=True) + shutil.copytree(boring / 'include/openssl', stage / 'include/openssl', dirs_exist_ok=True) + shutil.copytree(curl / 'include/curl', stage / 'include/curl', dirs_exist_ok=True) + shutil.copytree(includes, stage / 'include', dirs_exist_ok=True) + for source, name in [(curl_build / 'curl/lib/libcurl.a', 'curl'), + (boring_build / 'libssl.a', 'ssl'), (boring_build / 'libcrypto.a', 'crypto'), + (zlib_build / 'libz.a', 'zlib')]: + shutil.copyfile(source, stage / f'lib/lib{name}.a') + (stage / 'cmake').mkdir(exist_ok=True) + shutil.copyfile(ROOT / 'tool/ohos/EchHttpDeps.cmake', stage / 'cmake/EchHttpDeps.cmake') + (stage / 'metadata.json').write_text(json.dumps({ + 'schema': 1, 'target': f'ohos-{options.arch}', + 'curl': manifest['curl'], 'boringssl': manifest['boringssl'], 'zlib': manifest['zlib'], + 'native_sdk': json.loads((sdk / 'oh-uni-package.json').read_text()), + }, indent=2) + '\n') + if options.deps_only: + return + bridge = work / 'bridge' + run([cmake, '-S', ROOT / 'src', '-B', bridge, *common, + f'-DEH_DEPS_ROOT={stage}', f'-DEH_DART_INCLUDE={options.dart_sdk.resolve()}/include', + '-DEH_BUILD_SMOKE=ON']) + run([cmake, '--build', bridge, '--parallel', parallel]) + print(f'OpenHarmony library: {bridge}/out/libech_http.so', flush=True) + + +if __name__ == '__main__': + main() diff --git a/tool/check_native_build.dart b/tool/check_native_build.dart index 9f3f01f..614b336 100644 --- a/tool/check_native_build.dart +++ b/tool/check_native_build.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:io'; import '../hook/build.dart' as hook; +import 'package:code_assets/code_assets.dart'; +import 'package:ech_http/src/build_support/ohos.dart'; Future main(List args) async { if (args.length < 2 || args.length > 4) { @@ -11,6 +13,12 @@ Future main(List args) async { ); } final os = args[0], arch = args[1]; + final ohos = os == 'ohos' + ? OhosBuildConfig.detect( + targetOS: OS.fromString('ohos'), + architecture: Architecture.fromString(arch), + ) + : null; final simulator = args.length > 2 && args[2] == 'simulator'; final root = Directory.current.absolute.uri; final directory = Directory.fromUri( @@ -21,8 +29,18 @@ Future main(List args) async { await directory.create(recursive: true); final config = { 'link_mode_preference': 'dynamic', - 'target_os': os, + // Exercise the actual compatibility input supplied by Flutter-OH. + 'target_os': ohos == null ? os : 'linux', 'target_architecture': arch, + if (ohos != null) + 'c_compiler': { + 'cc': + '${ohos.nativeSdk}/llvm/bin/clang${Platform.isWindows ? '.exe' : ''}', + 'ar': + '${ohos.nativeSdk}/llvm/bin/llvm-ar${Platform.isWindows ? '.exe' : ''}', + 'ld': + '${ohos.nativeSdk}/llvm/bin/ld.lld${Platform.isWindows ? '.exe' : ''}', + }, if (os == 'android') 'android': {'target_ndk_api': 21}, if (os == 'ios') 'ios': { diff --git a/tool/create_flutter_smoke.dart b/tool/create_flutter_smoke.dart index a04688e..ff2e5e2 100644 --- a/tool/create_flutter_smoke.dart +++ b/tool/create_flutter_smoke.dart @@ -1,13 +1,19 @@ // Creates a disposable Flutter consumer to verify automatic native bundling. import 'dart:io'; -Future main() async { +Future main(List args) async { + if (args.isNotEmpty && (args.length != 1 || args.single != 'ohos')) { + throw ArgumentError( + 'Usage: dart run tool/create_flutter_smoke.dart [ohos]', + ); + } + final platforms = args.isEmpty ? 'android,ios,macos,windows,linux' : 'ohos'; final root = Directory.current.absolute.path; final app = Directory('build/flutter_smoke').absolute; final create = await Process.run('flutter', [ 'create', '--no-pub', - '--platforms=android,ios,macos,windows,linux', + '--platforms=$platforms', '--project-name=ech_http_smoke', app.path, ], runInShell: Platform.isWindows); @@ -25,26 +31,58 @@ dependencies: sdk: flutter ech_http: path: '$packagePath' + http: ^1.6.0 +dev_dependencies: + flutter_lints: ^6.0.0 '''); await File('${app.path}/lib/main.dart').writeAsString(''' import 'package:flutter/material.dart'; import 'package:ech_http/ech_http.dart'; +import 'package:http/http.dart' as http; void main() { - final client = EchClient(); - final result = client.get(Uri.https('example.com')).then((r) { - client.close(); - return '\${EchClient.backendVersion}: HTTP \${r.statusCode}'; - }, onError: (Object error) { client.close(); return error.toString(); }); - runApp(MaterialApp(home: Scaffold(body: Center(child: FutureBuilder( - future: result, - builder: (context, snapshot) => Text(snapshot.data ?? 'Loading'), + runApp(MaterialApp(home: Scaffold(body: Center(child: FutureBuilder( + future: smoke(), + builder: (context, snapshot) => Padding( + padding: const EdgeInsets.all(24), + child: SelectableText(snapshot.data ?? snapshot.error?.toString() ?? 'Testing ECH...'), + ), ))))); } + +Future smoke() async { + final target = Uri.https('crypto.cloudflare.com', '/cdn-cgi/trace'); + final bootstrap = EchClient(); + final client = EchClient(resolver: DohEchResolver( + client: bootstrap, + endpoint: Uri.https('cloudflare-dns.com', '/dns-query'), + hosts: {target.host}, + )); + try { + final response = await client.send(http.Request('GET', target)); + final body = await response.stream.bytesToString(); + final result = '\${EchClient.backendVersion}\\n' + 'HTTP \${response.statusCode}\\nECH accepted: \${response.echAccepted}\\n\\n\$body'; + debugPrint('ECH_HTTP_SMOKE: \$result'); + return result; + } finally { + client.close(); + bootstrap.close(); + } +} '''); // The generated counter-widget test no longer describes this consumer. final counterTest = File('${app.path}/test/widget_test.dart'); if (await counterTest.exists()) await counterTest.delete(); + if (platforms == 'ohos') { + final module = File('${app.path}/ohos/entry/src/main/module.json5'); + final source = await module.readAsString(); + if (!source.contains('"tablet"')) { + await module.writeAsString( + source.replaceFirst('"phone"', '"phone", "tablet"'), + ); + } + } stdout.writeln( 'Created ${app.path}. Run flutter pub get and flutter build there.', ); diff --git a/tool/native_smoke.cpp b/tool/native_smoke.cpp index 2b5cf32..4040340 100644 --- a/tool/native_smoke.cpp +++ b/tool/native_smoke.cpp @@ -2,6 +2,7 @@ #include "ech_http.h" #include #include +#include #include #include #include @@ -51,7 +52,10 @@ int main(int argc, char **argv) { bool accepted = false, ok = false; for (;;) { std::unique_lock lock(mutex); - ready.wait(lock, [] { return !events.empty(); }); + if (!ready.wait_for(lock, std::chrono::seconds(35), [] { return !events.empty(); })) { + std::fprintf(stderr, "timed out waiting for native response events\n"); + break; + } auto event = std::move(events.front()); events.pop_front(); lock.unlock(); @@ -70,5 +74,5 @@ int main(int argc, char **argv) { eh_request_destroy(request); eh_client_destroy(client); std::printf("body bytes=%zu\n", bytes); - return ok && accepted ? 0 : 1; + return ok && (argv[3][0] == '\0' || accepted) ? 0 : 1; } diff --git a/tool/ohos/CMakeLists.txt b/tool/ohos/CMakeLists.txt new file mode 100644 index 0000000..116ad18 --- /dev/null +++ b/tool/ohos/CMakeLists.txt @@ -0,0 +1,37 @@ +# The dependency options match ech-research/libechhttp-android-build (MIT, +# copyright 2026 Predidit). Use the same pinned headers and static libraries. +cmake_minimum_required(VERSION 3.22) +project(ech_http_ohos_dependencies LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(BUILD_LIBCURL_DOCS OFF CACHE BOOL "" FORCE) +set(BUILD_MISC_DOCS OFF CACHE BOOL "" FORCE) +set(ENABLE_CURL_MANUAL OFF CACHE BOOL "" FORCE) +set(CURL_USE_OPENSSL ON CACHE BOOL "" FORCE) +set(CURL_USE_SCHANNEL OFF CACHE BOOL "" FORCE) +set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE) +set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE) +set(CURL_USE_LIBSSH OFF CACHE BOOL "" FORCE) +set(CURL_USE_GSSAPI OFF CACHE BOOL "" FORCE) +set(CURL_ZLIB ON CACHE BOOL "" FORCE) +set(CURL_BROTLI OFF CACHE BOOL "" FORCE) +set(CURL_ZSTD OFF CACHE BOOL "" FORCE) +set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE) +set(USE_LIBIDN2 OFF CACHE BOOL "" FORCE) +set(USE_ECH ON CACHE BOOL "" FORCE) +set(CURL_CA_BUNDLE "none" CACHE STRING "" FORCE) +set(CURL_CA_PATH "none" CACHE STRING "" FORCE) +set(CURL_CA_FALLBACK OFF CACHE BOOL "" FORCE) +set(HTTP_ONLY ON CACHE BOOL "" FORCE) +set(OPENSSL_INCLUDE_DIR "${EH_BORINGSSL_SOURCE}/include" CACHE PATH "" FORCE) +set(OPENSSL_SSL_LIBRARY "${EH_BORINGSSL_BUILD}/libssl.a" CACHE FILEPATH "" FORCE) +set(OPENSSL_CRYPTO_LIBRARY "${EH_BORINGSSL_BUILD}/libcrypto.a" CACHE FILEPATH "" FORCE) +find_package(OpenSSL REQUIRED) +# curl's C configure probes also need BoringSSL's C++ runtime. +set_property(TARGET OpenSSL::SSL APPEND PROPERTY INTERFACE_LINK_LIBRARIES + ${CMAKE_CXX_IMPLICIT_LINK_LIBRARIES}) +find_package(ZLIB REQUIRED) +add_subdirectory("${EH_CURL_SOURCE}" curl) diff --git a/tool/ohos/EchHttpDeps.cmake b/tool/ohos/EchHttpDeps.cmake new file mode 100644 index 0000000..0854ab2 --- /dev/null +++ b/tool/ohos/EchHttpDeps.cmake @@ -0,0 +1,15 @@ +if(TARGET EchHttp::curl) + return() +endif() +get_filename_component(_eh_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) +foreach(_eh_lib curl ssl crypto zlib) + add_library(EchHttp::${_eh_lib} STATIC IMPORTED) + set_target_properties(EchHttp::${_eh_lib} PROPERTIES + IMPORTED_LOCATION "${_eh_root}/lib/lib${_eh_lib}.a" + INTERFACE_INCLUDE_DIRECTORIES "${_eh_root}/include") +endforeach() +find_package(Threads REQUIRED) +set_property(TARGET EchHttp::curl PROPERTY INTERFACE_COMPILE_DEFINITIONS CURL_STATICLIB) +set_property(TARGET EchHttp::curl PROPERTY INTERFACE_LINK_LIBRARIES EchHttp::ssl EchHttp::zlib) +set_property(TARGET EchHttp::ssl PROPERTY INTERFACE_LINK_LIBRARIES EchHttp::crypto) +set_property(TARGET EchHttp::crypto PROPERTY INTERFACE_LINK_LIBRARIES Threads::Threads ${CMAKE_DL_LIBS} m) From c7c9132dfae816680df955a86f95f7b5e36704ad Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 04:24:28 +0800 Subject: [PATCH 2/8] fix(build): refresh CMake caches when source paths change --- hook/build.dart | 3 ++ lib/src/build_support/cmake.dart | 24 ++++++++++++++++ test/cmake_build_test.dart | 47 ++++++++++++++++++++++++++++++++ tool/build_ohos.py | 22 ++++++++++++--- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 lib/src/build_support/cmake.dart create mode 100644 test/cmake_build_test.dart diff --git a/hook/build.dart b/hook/build.dart index 960b735..cbcd96b 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -9,6 +9,7 @@ import 'package:path/path.dart' as p; import 'package:ech_http/src/build_support/prebuilt.dart'; import 'package:ech_http/src/build_support/ohos.dart'; +import 'package:ech_http/src/build_support/cmake.dart'; Future main(List args) async { await build(args, (input, output) async { @@ -56,6 +57,7 @@ Future main(List args) async { Uri.file(manifestPath), Uri.file(p.join(root, 'lib', 'src', 'build_support', 'prebuilt.dart')), Uri.file(p.join(root, 'lib', 'src', 'build_support', 'ohos.dart')), + Uri.file(p.join(root, 'lib', 'src', 'build_support', 'cmake.dart')), ]); await for (final file in Directory( p.join(root, 'src'), @@ -129,6 +131,7 @@ Future main(List args) async { output.dependencies.add(Uri.file(p.join(dartInclude, header))); } final nativeBuild = p.join(out, 'native'); + await prepareCmakeDirectory(p.join(root, 'src'), nativeBuild); stderr.writeln('ech_http: compiling C++ bridge with $target dependencies'); await runner.run(tools.cmake, [ '-S', diff --git a/lib/src/build_support/cmake.dart b/lib/src/build_support/cmake.dart new file mode 100644 index 0000000..b67fa5c --- /dev/null +++ b/lib/src/build_support/cmake.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +/// CMake caches absolute source paths. Hooks can reuse their output directory +/// when a dependency moves from a local checkout to a versioned pub cache. +Future prepareCmakeDirectory(String source, String build) async { + final cache = File(p.join(build, 'CMakeCache.txt')); + if (!await cache.exists()) return; + const prefix = 'CMAKE_HOME_DIRECTORY:INTERNAL='; + final homes = (await cache.readAsLines()).where( + (line) => line.startsWith(prefix), + ); + if (homes.isEmpty || + p.equals( + p.normalize(homes.single.substring(prefix.length)), + p.normalize(p.absolute(source)), + )) { + return; + } + await cache.delete(); + final files = Directory(p.join(build, 'CMakeFiles')); + if (await files.exists()) await files.delete(recursive: true); +} diff --git a/test/cmake_build_test.dart b/test/cmake_build_test.dart new file mode 100644 index 0000000..b4c7743 --- /dev/null +++ b/test/cmake_build_test.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:ech_http/src/build_support/cmake.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + late Directory root; + late String source; + late String build; + late File cache; + late File metadata; + late File artifact; + + setUp(() async { + root = await Directory.systemTemp.createTemp('ech-cmake-'); + source = p.join(root.path, 'checkout', 'src'); + build = p.join(root.path, 'build'); + await Directory(p.join(build, 'CMakeFiles')).create(recursive: true); + cache = File(p.join(build, 'CMakeCache.txt')); + metadata = File(p.join(build, 'CMakeFiles', 'stamp')); + artifact = File(p.join(build, 'libech_http.so')); + await metadata.writeAsString('generated'); + await artifact.writeAsString('compiled library'); + }); + tearDown(() => root.delete(recursive: true)); + + test('keeps an incremental build at the same source location', () async { + await cache.writeAsString('CMAKE_HOME_DIRECTORY:INTERNAL=$source\n'); + await prepareCmakeDirectory(source, build); + expect(await cache.exists(), isTrue); + expect(await metadata.exists(), isTrue); + }); + + test('local-to-Git dependency changes discard only CMake metadata', () async { + await cache.writeAsString('CMAKE_HOME_DIRECTORY:INTERNAL=$source\n'); + await prepareCmakeDirectory(p.join(root.path, 'pub-cache', 'src'), build); + expect(await cache.exists(), isFalse); + expect(await metadata.exists(), isFalse); + expect(await artifact.readAsString(), 'compiled library'); + }); + + test('accepts a build directory without a cache', () async { + await prepareCmakeDirectory(source, build); + expect(await artifact.exists(), isTrue); + }); +} diff --git a/tool/build_ohos.py b/tool/build_ohos.py index 6034e07..d447198 100644 --- a/tool/build_ohos.py +++ b/tool/build_ohos.py @@ -24,6 +24,20 @@ def run(args): subprocess.run(list(map(str, args)), check=True) +def configure(cmake, source, build, arguments): + # A Git dependency update changes the package root while the hook's shared + # build directory stays put. Preserve downloads/libraries, reset metadata. + cache = build / 'CMakeCache.txt' + prefix = 'CMAKE_HOME_DIRECTORY:INTERNAL=' + if cache.is_file(): + homes = [line[len(prefix):] for line in cache.read_text().splitlines() + if line.startswith(prefix)] + if homes and Path(homes[0]).resolve() != source.resolve(): + cache.unlink() + shutil.rmtree(build / 'CMakeFiles', ignore_errors=True) + run([cmake, '-S', source, '-B', build, *arguments]) + + def digest(path): with path.open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() @@ -109,11 +123,11 @@ def main(): boring, curl, zlib = sources(manifest, options.source_cache.resolve()) parallel = str(min(os.cpu_count() or 2, 8)) boring_build = work / 'boringssl' - run([cmake, '-S', boring, '-B', boring_build, *common, '-DBUILD_SHARED_LIBS=OFF', + configure(cmake, boring, boring_build, [*common, '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_TESTING=OFF', '-DOPENSSL_NO_ASM=ON', '-DOPENSSL_SMALL=ON']) run([cmake, '--build', boring_build, '--target', 'ssl', 'crypto', '--parallel', parallel]) zlib_build = work / 'zlib' - run([cmake, '-S', zlib, '-B', zlib_build, *common, '-DZLIB_BUILD_SHARED=OFF', + configure(cmake, zlib, zlib_build, [*common, '-DZLIB_BUILD_SHARED=OFF', '-DZLIB_BUILD_STATIC=ON', '-DZLIB_BUILD_TESTING=OFF']) run([cmake, '--build', zlib_build, '--target', 'zlibstatic', '--parallel', parallel]) includes = work / 'zlib-include' @@ -121,7 +135,7 @@ def main(): shutil.copyfile(zlib / 'zlib.h', includes / 'zlib.h') shutil.copyfile(zlib_build / 'zconf.h', includes / 'zconf.h') curl_build = work / 'curl' - run([cmake, '-S', ROOT / 'tool/ohos', '-B', curl_build, *common, + configure(cmake, ROOT / 'tool/ohos', curl_build, [*common, f'-DEH_CURL_SOURCE={curl}', f'-DEH_BORINGSSL_SOURCE={boring}', f'-DEH_BORINGSSL_BUILD={boring_build}', f'-DZLIB_LIBRARY={zlib_build}/libz.a', f'-DZLIB_INCLUDE_DIR={includes}']) @@ -149,7 +163,7 @@ def main(): if options.deps_only: return bridge = work / 'bridge' - run([cmake, '-S', ROOT / 'src', '-B', bridge, *common, + configure(cmake, ROOT / 'src', bridge, [*common, f'-DEH_DEPS_ROOT={stage}', f'-DEH_DART_INCLUDE={options.dart_sdk.resolve()}/include', '-DEH_BUILD_SMOKE=ON']) run([cmake, '--build', bridge, '--parallel', parallel]) From c82017801a51addac84cf26545a1f7e03af577be Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 05:18:17 +0800 Subject: [PATCH 3/8] fix(ohos): invalidate dependency caches and isolate CI Dart SDKs --- .github/workflows/ci.yml | 6 +- .pubignore | 1 + CHANGELOG.md | 21 +++++ README.md | 10 +- README.zh-CN.md | 8 +- doc/ohos.md | 192 +++++++++++++++++++++++++++++++++++++++ doc/ohos.zh-CN.md | 37 +++++++- doc/verification.md | 2 +- tool/build_ohos.py | 40 ++++---- tool/test_build_ohos.py | 74 +++++++++++++++ 10 files changed, 362 insertions(+), 29 deletions(-) create mode 100644 doc/ohos.md create mode 100644 tool/test_build_ohos.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7055d36..3c196c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,7 @@ jobs: version: 26.0.0.821 cache: true - run: dart pub get + - run: python3 tool/test_build_ohos.py - run: dart test test/ohos_build_test.dart -r expanded - name: Cross-compile using the Flutter-OH compatibility protocol shell: bash @@ -134,13 +135,16 @@ jobs: - name: Set up the verified Flutter-OH toolchain shell: bash run: | + echo "ECH_HTTP_DART=$(command -v dart)" >> "$GITHUB_ENV" git init "$RUNNER_TEMP/flutter-ohos" git -C "$RUNNER_TEMP/flutter-ohos" remote add origin https://gitcode.com/CPF-Flutter/flutter_flutter.git git -C "$RUNNER_TEMP/flutter-ohos" fetch --depth=1 origin 62357a93d653bf844f730bb1f4b7cc9e2139d14e git -C "$RUNNER_TEMP/flutter-ohos" checkout --detach FETCH_HEAD echo "$RUNNER_TEMP/flutter-ohos/bin" >> "$GITHUB_PATH" echo "CI=true" >> "$GITHUB_ENV" - - run: dart run tool/create_flutter_smoke.dart ohos + # Keep the package's hook kernel on the SDK that compiled it. The + # consumer below has a separate .dart_tool and uses Flutter-OH's Dart. + - run: '"$ECH_HTTP_DART" run tool/create_flutter_smoke.dart ohos' - name: Build a Flutter consumer HAP with the native assets working-directory: build/flutter_smoke run: | diff --git a/.pubignore b/.pubignore index ddb96a8..ffefb89 100644 --- a/.pubignore +++ b/.pubignore @@ -7,3 +7,4 @@ coverage/ pubspec.lock *.jpg *.log +__pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b600d..516aed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] + +### Added +- OpenHarmony / HarmonyOS arm64 and x64 builds through Flutter-OH Native Assets, + using pinned BoringSSL, curl, and zlib sources with verified downloads. +- OHOS SDK detection, cross-compilation and Flutter HAP smoke checks in CI, + and English and Chinese setup and verification guides. + +### Changed +- Allow compatible `code_assets` and `hooks` versions for Flutter-OH SDKs that + pin `meta` 1.17.0. + +### Fixed +- Invalidate OHOS CMake outputs when dependency pins, source paths, compiler + options, or the native SDK change, while retaining downloaded sources. +- Reset bridge CMake metadata when the package moves between checkouts or caches. +- Use the original Dart SDK for CI package tools after adding Flutter-OH to PATH, + avoiding reuse of an incompatible hook kernel across Dart SDK versions. + +--- + ## [0.2.1] - 2026-09-29 ### Changed diff --git a/README.md b/README.md index 3a9f70a..72e4009 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # ech_http -This fork adds [OpenHarmony / HarmonyOS build support](doc/ohos.zh-CN.md) +ech_http supports [OpenHarmony / HarmonyOS builds](doc/ohos.md) using Flutter-OH Native Assets and pinned source builds of the native dependencies. See that guide for toolchain requirements and verification limits. [![Dart SDK](https://img.shields.io/badge/Dart-3.10+-0175C2.svg?logo=dart)](https://dart.dev) [![Flutter](https://img.shields.io/badge/Flutter-3.38+-02569B.svg?logo=flutter)](https://flutter.dev) -[![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-blue.svg)](#supported-platforms-and-toolchain) +[![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows%20%7C%20OHOS-blue.svg)](#supported-platforms-and-toolchain) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![ECH Standard](https://img.shields.io/badge/TLS%201.3-ECH%20(RFC%20Draft)-green.svg)](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/) @@ -40,6 +40,8 @@ flowchart TD Hook["hook/build.dart"] -->|"Verify SHA-256"| SDKCache["Precompiled SDK Cache\n(.dart_tool or user_defines)"] SDKCache -->|"Fetch Target Only"| PrebuiltRepo["GitHub Releases\n(Windows / Linux / Apple / Android)"] Hook -->|"Compile C++ Bridge Only"| CMakeNinja["CMake + Ninja"] + Hook -->|"OHOS: Build Pinned Sources"| OhosSources["BoringSSL + curl + zlib"] + OhosSources --> CMakeNinja CMakeNinja -->|"Bundle Native Library"| OutputLib["ech_http.dll / .so / .dylib"] end ``` @@ -63,7 +65,7 @@ prompt cleanup. - **Authenticated ECH Retries**: Supports up to 2 automated ECH retries when the server responds with a valid TLS `retry_configs` rejection. Public names are strictly validated before retrying. - **Separation of Destination Routing and SNI**: IP address overrides (`addresses`) redirect only the underlying TCP socket or proxy CONNECT tunnel. The TLS inner SNI, certificate identity verification, and HTTP `Host` header always correspond to the original requested hostname. - **Strict TLS Verification**: Every connection strictly validates the server's certificate chain and hostname against the bundled Mozilla CA root certificates. There is **no insecure mode** or certificate bypass switch. Custom PKI roots can be explicitly loaded via `trustedRootsPem`. -- **High-Performance Build Hooks**: Build hooks (`hook/build.dart`) automatically download verified precompiled dependency SDKs for the exact target platform, compiling only the lightweight C++ bridge locally (~15 s cold build). No need to compile BoringSSL or libcurl from scratch. +- **Automatic Native Builds**: Build hooks (`hook/build.dart`) download verified precompiled dependency SDKs for Windows, Linux, Apple, and Android, compiling only the lightweight C++ bridge locally (~15 s cold build). OHOS builds pinned BoringSSL, curl, and zlib sources on the first build and reuses them incrementally. - **Zero Runtime Daemons**: Communicates directly through Dart FFI within the host process memory space, avoiding child processes, socket proxy bottlenecks, and OS port conflicts. --- @@ -77,7 +79,7 @@ prompt cleanup. | **macOS** | `x64`, `arm64` | macOS host, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (device), `arm64`/`x64` (simulator) | macOS host, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (or Flutter bundled NDK), CMake, Ninja | Android API level 21+ (5.0 Lollipop) | -| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, Python 3.12+, Git, Go, CMake, Ninja; Flutter-OH Native Assets for HAPs | See [OHOS validation](doc/ohos.zh-CN.md) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, Python 3.12+, Git, Go, CMake, Ninja; Flutter-OH Native Assets for HAPs | See [OHOS validation](doc/ohos.md) | > [!NOTE] > Web (Browsers) is not supported. OHOS builds its pinned dependencies from source on the first build; the other platforms use prebuilt SDKs. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. diff --git a/README.zh-CN.md b/README.zh-CN.md index 4e4c264..3214061 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,13 +2,13 @@ [![Dart SDK](https://img.shields.io/badge/Dart-3.10+-0175C2.svg?logo=dart)](https://dart.dev) [![Flutter](https://img.shields.io/badge/Flutter-3.38+-02569B.svg?logo=flutter)](https://flutter.dev) -[![Platforms](https://img.shields.io/badge/平台支持-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-blue.svg)](#支持平台与工具链) +[![Platforms](https://img.shields.io/badge/平台支持-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows%20%7C%20OHOS-blue.svg)](#支持平台与工具链) [![License: MIT](https://img.shields.io/badge/开源协议-MIT-yellow.svg)](LICENSE) [![ECH Standard](https://img.shields.io/badge/TLS%201.3-ECH%20(RFC%20Draft)-green.svg)](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/) [English](README.md) -本分支新增 [OpenHarmony / HarmonyOS 构建支持](doc/ohos.zh-CN.md), +`ech_http` 支持 [OpenHarmony / HarmonyOS 构建](doc/ohos.zh-CN.md), 通过 Flutter-OH Native Assets 从固定版本源码构建原生依赖,详见接入与验证说明。 `ech_http` 是专为 Dart 和 Flutter 打造的企业级 HTTP 客户端,深度集成了 **TLS 密文客户端问候(Encrypted Client Hello, ECH)**、HTTP CONNECT 代理、自定义 PKI 信任根及流式数据传输功能。完全兼容官方 `package:http.Client` 接口规范。 @@ -39,6 +39,8 @@ flowchart TD Hook["hook/build.dart 构建钩子"] -->|"校验 SHA-256 摘要"| SDKCache["预编译 SDK 本地缓存\n(.dart_tool 或 user_defines)"] SDKCache -->|"仅下载当前构建目标"| PrebuiltRepo["GitHub Releases\n(Windows / Linux / Apple / Android)"] Hook -->|"仅就地编译 C++ 桥接代码"| CMakeNinja["CMake + Ninja"] + Hook -->|"OHOS:编译固定版本源码"| OhosSources["BoringSSL + curl + zlib"] + OhosSources --> CMakeNinja CMakeNinja -->|"打包原生动态库"| OutputLib["ech_http.dll / .so / .dylib"] end ``` @@ -59,7 +61,7 @@ flowchart TD - **原生支持认证重试(Authenticated Retry)**:当服务端返回合法的 TLS `retry_configs`(配置拒绝重试)时,客户端会自动进行最多 2 次认证重试。重试前会严格校验外层公用名(Public Name)以防篡改。 - **物理路由与证书身份严格解耦**:自定义目标 IP 地址列表(`addresses`)仅重定向底层的 TCP Socket 物理连接或代理 CONNECT 隧道;TLS 握手中的内层 SNI、服务端证书主体身份核验以及 HTTP `Host` 请求头始终保持原始目标域名不变。 - **严格证书校验,无旁路后门**:所有连接默认基于内置的 Mozilla CA 根证书库严格验证证书链与主机名。**库中未设置任何跳过证书验证的不安全开关**。如需使用企业私有 PKI,可通过 `trustedRootsPem` 显式替换信任根。 -- **极速构建钩子机制**:借助 Dart `hook/build.dart`,在首次构建时按需拉取对应平台的预编译 SDK,本地仅需编译轻量级的 C++ 桥接代码(冷构建仅约 15 秒),无需在本地完整编译庞大的 BoringSSL 与 libcurl。 +- **自动原生构建**:借助 Dart `hook/build.dart`,Windows、Linux、Apple 和 Android 按需拉取经过校验的预编译 SDK,本地仅编译轻量级 C++ 桥接代码(冷构建约 15 秒)。OHOS 首次构建编译固定版本的 BoringSSL、curl 和 zlib,后续增量复用产物。 - **零外部守护进程**:直接在应用主进程内存中通过 Dart FFI 调用原生接口,避免了子进程开销、本地端口占用冲突及系统级代理注入风险。 --- diff --git a/doc/ohos.md b/doc/ohos.md new file mode 100644 index 0000000..3ba082d --- /dev/null +++ b/doc/ohos.md @@ -0,0 +1,192 @@ +# OpenHarmony / HarmonyOS native support + +[English](ohos.md) | [简体中文](ohos.zh-CN.md) + +Build and bundle `ech_http` through Flutter-OH Native Assets on `arm64` +(phones and tablets) and `x64`. The existing `EchClient`, ECH, certificate +validation, proxy, gzip, and streaming APIs remain available. See the +verification scope below for the checks performed on each architecture. + +## Requirements + +- Dart >=3.10, Python >=3.12, Git, CMake >=3.22, Ninja, and Go (for BoringSSL). +- An OpenHarmony native SDK containing `build/cmake/ohos.toolchain.cmake`. + `OHOS_NDK_HOME` may point to the native directory or its parent. + The HarmonyOS command-line tools layout + `HOS_SDK_HOME/default/openharmony/native` is also supported. +- Flutter-OH with OHOS Native Assets support for application builds. + Verified with CPF-Flutter `oh-3.41.9-release`, commit + `62357a93d653bf844f730bb1f4b7cc9e2139d14e`. + Earlier toolchains with only FFI plugin packaging are insufficient. + +The compatible `code_assets` and `hooks` version ranges accommodate Flutter-OH +3.41's `meta 1.17.0` pin. The verified consumer resolves `code_assets 1.2.1` +and `hooks 2.0.2`, without overriding Flutter's `meta` dependency. + +The verified Flutter-OH toolchain passes `target_os=linux` to third-party hooks +alongside its `native/llvm/bin/clang`. The hook identifies OHOS from that +compiler's SDK and uses the OHOS toolchain and sysroot, rather than Linux +prebuilt dependencies. Explicit `target_os=ohos` inputs are also recognized. +Installing an OHOS SDK alone does not redirect ordinary Linux builds. + +## Application setup + +Reference an `ech_http` version containing OHOS support from the Flutter-OH +application's `pubspec.yaml`. For development and verification, use a local +dependency: + +```yaml +dependencies: + ech_http: + path: /absolute/path/to/ech_http +``` + +Run `flutter pub get` and `flutter build hap` with Flutter-OH. +The hook normally locates the SDK from Flutter's compiler path. If that path +is unavailable, configure the target explicitly in the **application's** +`pubspec.yaml`: + +```yaml +hooks: + user_defines: + ech_http: + target: ohos + ohos_sdk: /absolute/path/to/openharmony/native + # Optional: share downloaded pinned sources between builds. + source_cache: /absolute/path/to/ech-http-sources +``` + +The first build downloads and compiles the BoringSSL, curl, and zlib versions +pinned in `lib/src/build_support/dependencies.json`. curl and zlib archives +are checked against SHA-256 digests; BoringSSL is checked against its Git +revision. Later builds reuse Ninja outputs incrementally. No additional +prebuilt dependency repository is required. + +OHOS links the C++ runtime statically and hides static library symbols. The +HAP does not need an extra `libc++_shared.so`, and the TLS library's symbols +are not exported to other native plugins. + +## Standalone builds + +Check cross-compilation and Native Assets output without creating an app: + +```sh +export OHOS_NDK_HOME=/absolute/path/to/openharmony/native +dart pub get +dart run tool/check_native_build.dart ohos arm64 +``` + +The check uses Flutter-OH's Linux compatibility input and OHOS compiler. +The manifest at `.dart_tool/verify/ohos-arm64-device/output.json` identifies +the OHOS library as `package:ech_http/ech_http_bindings`. +Repeat with `ohos x64` to verify the other supported architecture. + +To build the library and native request smoke executable directly: + +```sh +python3 tool/build_ohos.py \ + --native-sdk "$OHOS_NDK_HOME" \ + --dart-sdk /absolute/path/to/dart-sdk \ + --arch arm64 +``` + +Outputs are in `build/ohos/arm64/bridge/out/`. `--arch x64` uses +`build/ohos/x64/`. Flutter-OH builds run the same dependency build steps. + +The native smoke executable accepts four arguments: + +```sh +./ech_http_smoke URL HTTP_PROXY_OR_EMPTY ECH_CONFIG_LIST_OR_EMPTY TARGET_IP_OR_EMPTY +``` + +With a nonempty ECH configuration, success requires HTTP 200 and accepted ECH. +Commercial HarmonyOS devices may reject unsigned ELF executables. Use an +official debug signature or a normally signed test HAP; executable file +permissions do not replace code signing. + +## Flutter device smoke app + +```sh +dart run tool/create_flutter_smoke.dart ohos +cd build/flutter_smoke +flutter pub get +flutter build hap --debug --no-codesign +``` + +The app's bundle name is `com.example.ech_http_smoke`, with phone and tablet +support. It discovers Cloudflare's ECH configuration over DoH, sends an HTTPS +request, and displays the status, `ECH accepted`, and backend version. Sign +with the normal Huawei debug signing workflow before installing on a device. +Success requires HTTP 200 and `ECH accepted: true`. + +The unsigned HAP is at +`build/flutter_smoke/build/ohos/hap/entry-default-unsigned.hap`, relative to +the repository root. The verified Flutter-OH macOS Release AOT compiler is +Intel-only, so Release builds on Apple Silicon require Rosetta. Debug smoke +builds do not use that AOT compiler. + +## Verification scope + +Local verification on 2026-10-17 used a macOS arm64 host, Dart 3.13.5, +and OpenHarmony native SDK 6.0.0.47 (API 20). The Flutter HAP used +Flutter-OH 3.41.9+ohos-1.0.2, its bundled Dart 3.11.5, and HarmonyOS SDK +26.0.0.105. The connected MatePad mini runs HarmonyOS API 26. + +| Check | Result | +| --- | --- | +| arm64 native library and smoke executable | Cross-compilation passed | +| x64 native library and smoke executable | Cross-compilation passed; no x64 device attached | +| Flutter-OH compatibility input | Build hook produced the Native Assets manifest | +| ELF dynamic dependencies | Only `libc.so`; no additional TLS, zlib, or C++ shared library | +| macOS offline unit and native regression tests | Passed; public-network tests are opt-in | +| Flutter-OH consumer dependency resolution | Passed with Flutter's `meta 1.17.0` pin | +| Flutter smoke app analysis | Passed | +| Debug HAP packaging | Passed with arm64 `libech_http.so` and Native Assets mapping | +| Local Release HAP build | Native library and Dart compilation passed; AOT requires Rosetta, absent on the verified host | +| Bare ELF execution on MatePad mini | Rejected with `Permission denied` before request code ran | +| Signed Debug HAP on MatePad mini | Native library loaded and FFI symbols resolved | +| Device HTTPS / ECH request | HTTP 200, accepted ECH, `sni=encrypted`, and `tls=TLSv1.3` | + +The signed HAP's device log contains: + +```text +ECH_HTTP_SMOKE: libcurl/8.22.0 BoringSSL zlib/1.3.2 +HTTP 200 +ECH accepted: true +tls=TLSv1.3 +sni=encrypted +``` + +The local capture is stored in `build/ohos-runtime-smoke.log` (a generated +file, not part of the package). Device verification covers Native Assets +integration and public HTTPS/ECH. gzip, proxies, cancellation, and isolate +shutdown have desktop regression coverage but have not each been repeated +on an OHOS device. x64 has build coverage only. Release HAP and remote CI +verification were not part of the initial local device run. + +## Caching and Dart SDK changes + +Build directories are separate for each architecture. The source builder +records each dependency and standalone bridge configuration's source path, configure +arguments, dependency pins, native SDK metadata, and toolchain digest. +Unchanged inputs preserve incremental outputs. Changed inputs discard the +generated directory and repeat configuration probes and compilation. This +also migrates older build directories without a configuration record. +Downloaded sources are stored separately and survive this invalidation. +The build hook also resets bridge CMake metadata when the package source moves. + +When switching between standard Dart and Flutter-OH's bundled Dart, +regenerate the application's `.dart_tool` and run `flutter pub get`. +Otherwise a cached `hook.dill` may produce +`Invalid kernel binary format version`. CI keeps package tools on their +original Dart SDK and gives the Flutter-OH consumer its own `.dart_tool`. + +Run the cache regression checks without networking or an OHOS SDK: + +```sh +python3 tool/test_build_ohos.py +``` + +The OHOS CI job checks SDK detection, cache invalidation, arm64/x64 hook +outputs, and an unsigned Release HAP built with the pinned Flutter-OH +toolchain. Remote packaging checks do not replace signed device execution. diff --git a/doc/ohos.zh-CN.md b/doc/ohos.zh-CN.md index 4de3948..36269ef 100644 --- a/doc/ohos.zh-CN.md +++ b/doc/ohos.zh-CN.md @@ -1,6 +1,8 @@ # OpenHarmony / HarmonyOS 原生适配 -本分支支持通过 Flutter-OH 的 Native Assets 构建和打包 `ech_http`。 +[English](ohos.md) | [简体中文](ohos.zh-CN.md) + +支持通过 Flutter-OH 的 Native Assets 构建和打包 `ech_http`。 保留现有 `EchClient`、ECH、证书校验、代理、gzip 和流式响应 API。 支持 `arm64`(手机、平板)和 `x64`;各架构的验证结果见下文。 @@ -15,7 +17,7 @@ `62357a93d653bf844f730bb1f4b7cc9e2139d14e` 已包含该功能。 仅有旧版 FFI 插件打包支持的 Flutter-OH 不满足要求。 -本分支允许使用 `code_assets` / `hooks` 的兼容版本范围,解决 Flutter-OH +包允许使用 `code_assets` / `hooks` 的兼容版本范围,解决 Flutter-OH 3.41 固定 `meta 1.17.0` 与 `hooks 2.2` 的依赖冲突。实际测试应用解析为 `code_assets 1.2.1`、`hooks 2.0.2`,无需覆盖 Flutter SDK 的 `meta` 版本。 @@ -26,7 +28,8 @@ OHOS toolchain、sysroot 和架构编译,**不会使用 Linux 预编译依赖* ## 应用接入 -在 Flutter-OH 应用的 `pubspec.yaml` 中使用本分支,例如本地依赖: +在 Flutter-OH 应用的 `pubspec.yaml` 中引用包含 OHOS 支持的 `ech_http`。 +开发与验证时可使用本地依赖: ```yaml dependencies: @@ -146,4 +149,30 @@ sni=encrypted 本地验证记录保存在 `build/ohos-runtime-smoke.log`。本次已验证 Flutter Native Assets 集成和公网 HTTPS/ECH;gzip、代理、取消请求、isolate 退出 等行为已覆盖桌面回归测试,尚未在鸿蒙真机上逐项复测。 -x64 仅完成交叉编译和构建钩子检查,Release HAP 及远程 CI 尚未运行验证。 +x64 仅完成交叉编译和构建钩子检查。初次本地真机验证未包含 Release HAP +与远程 CI;后续远程验证结果见下文记录。 + +## 缓存与工具链切换 + +构建目录按架构区分。源码构建脚本为每个依赖和独立编译的桥接层记录源码绝对路径、 +配置参数、BoringSSL/curl/zlib 版本与摘要、SDK 元数据及 toolchain 摘要。 +这些输入不变时复用 Ninja 增量产物;输入改变时清理对应的生成目录, +重新运行配置探测和编译,避免依赖升级后出现源码目录冲突或使用旧探测结果。 +迁移没有配置记录的旧构建目录时也会重新生成。 +已下载的源码独立保存,不受 CMake 构建目录清理影响。 +构建钩子的桥接层也会在包源码路径变更时重置 CMake 元数据。 + +切换标准 Dart 与 Flutter-OH 内置 Dart 时,应重新生成应用的 `.dart_tool` +并运行 `flutter pub get`,避免旧 SDK 编译的 `hook.dill` 导致 +`Invalid kernel binary format version`。CI 中包级检查始终使用原 Dart SDK, +Flutter-OH 测试应用使用自身独立的 `.dart_tool` 和内置 Dart。 + +运行无需网络或 OHOS SDK 的缓存回归检查: + +```sh +python3 tool/test_build_ohos.py +``` + +CI 的 OHOS 作业检查 SDK 识别、缓存失效、arm64/x64 构建钩子输出, +以及固定 Flutter-OH 工具链的未签名 Release HAP 打包。远程构建不能 +替代签名 HAP 的真机运行检查。 diff --git a/doc/verification.md b/doc/verification.md index ccb722d..95667aa 100644 --- a/doc/verification.md +++ b/doc/verification.md @@ -42,7 +42,7 @@ We distinguish between three tiers of verification: | **Android** | `arm64-v8a` | **Tier 1** | Physical device run (API 35), live ECH handshake, authenticated retry, connection reuse | Non-standard custom Android ROMs | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK packaging for both ABIs | Runtime execution on physical 32-bit ARM / x86_64 devices | | **Android** | `x86` | **Tier 2** | NDK bridge build and link check in CI | 32-bit x86 Android runtime execution | -| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | Both architectures cross-compile through the build hook; a signed arm64 Flutter Debug HAP returns HTTP 200 with ECH accepted on MatePad mini | See the [OHOS guide](ohos.zh-CN.md) for runtime scope; full suite not repeated on OHOS, no x64 device attached | +| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | Both architectures cross-compile through the build hook; a signed arm64 Flutter Debug HAP returns HTTP 200 with ECH accepted on MatePad mini | See the [OHOS guide](ohos.md) for runtime scope; full suite not repeated on OHOS, no x64 device attached | | **Web** | All | **Tier 3** | Not supported by this native C++ FFI architecture | N/A | --- diff --git a/tool/build_ohos.py b/tool/build_ohos.py index d447198..1cf9ad5 100644 --- a/tool/build_ohos.py +++ b/tool/build_ohos.py @@ -24,18 +24,20 @@ def run(args): subprocess.run(list(map(str, args)), check=True) -def configure(cmake, source, build, arguments): - # A Git dependency update changes the package root while the hook's shared - # build directory stays put. Preserve downloads/libraries, reset metadata. - cache = build / 'CMakeCache.txt' - prefix = 'CMAKE_HOME_DIRECTORY:INTERNAL=' - if cache.is_file(): - homes = [line[len(prefix):] for line in cache.read_text().splitlines() - if line.startswith(prefix)] - if homes and Path(homes[0]).resolve() != source.resolve(): - cache.unlink() - shutil.rmtree(build / 'CMakeFiles', ignore_errors=True) +def configure(cmake, source, build, arguments, identity=None): + # Configure probes and object files must not survive a dependency or SDK + # change. Downloads live outside these generated build directories. + fingerprint = json.dumps({ + 'schema': 1, + 'source': str(source.resolve()), + 'arguments': list(map(str, arguments)), + 'identity': identity, + }, sort_keys=True) + stamp = build / '.ech-http-config.json' + if build.exists() and (not stamp.is_file() or stamp.read_text() != fingerprint): + shutil.rmtree(build) run([cmake, '-S', source, '-B', build, *arguments]) + stamp.write_text(fingerprint) def digest(path): @@ -120,15 +122,21 @@ def main(): '-DCMAKE_C_FLAGS=-Wno-unused-command-line-argument', '-DCMAKE_CXX_FLAGS=-Wno-unused-command-line-argument'] manifest = json.loads((ROOT / 'lib/src/build_support/dependencies.json').read_text()) + sdk_metadata = json.loads((sdk / 'oh-uni-package.json').read_text()) + identity = { + 'dependencies': {name: manifest[name] for name in ('boringssl', 'curl', 'zlib')}, + 'native_sdk': sdk_metadata, + 'toolchain_sha256': digest(sdk / 'build/cmake/ohos.toolchain.cmake'), + } boring, curl, zlib = sources(manifest, options.source_cache.resolve()) parallel = str(min(os.cpu_count() or 2, 8)) boring_build = work / 'boringssl' configure(cmake, boring, boring_build, [*common, '-DBUILD_SHARED_LIBS=OFF', - '-DBUILD_TESTING=OFF', '-DOPENSSL_NO_ASM=ON', '-DOPENSSL_SMALL=ON']) + '-DBUILD_TESTING=OFF', '-DOPENSSL_NO_ASM=ON', '-DOPENSSL_SMALL=ON'], identity) run([cmake, '--build', boring_build, '--target', 'ssl', 'crypto', '--parallel', parallel]) zlib_build = work / 'zlib' configure(cmake, zlib, zlib_build, [*common, '-DZLIB_BUILD_SHARED=OFF', - '-DZLIB_BUILD_STATIC=ON', '-DZLIB_BUILD_TESTING=OFF']) + '-DZLIB_BUILD_STATIC=ON', '-DZLIB_BUILD_TESTING=OFF'], identity) run([cmake, '--build', zlib_build, '--target', 'zlibstatic', '--parallel', parallel]) includes = work / 'zlib-include' includes.mkdir(exist_ok=True) @@ -138,7 +146,7 @@ def main(): configure(cmake, ROOT / 'tool/ohos', curl_build, [*common, f'-DEH_CURL_SOURCE={curl}', f'-DEH_BORINGSSL_SOURCE={boring}', f'-DEH_BORINGSSL_BUILD={boring_build}', f'-DZLIB_LIBRARY={zlib_build}/libz.a', - f'-DZLIB_INCLUDE_DIR={includes}']) + f'-DZLIB_INCLUDE_DIR={includes}'], identity) run([cmake, '--build', curl_build, '--parallel', parallel]) if 'HAVE_SSL_SET1_ECH_CONFIG_LIST:INTERNAL=1' not in (curl_build / 'CMakeCache.txt').read_text(): raise RuntimeError('libcurl was built without BoringSSL ECH support') @@ -158,14 +166,14 @@ def main(): (stage / 'metadata.json').write_text(json.dumps({ 'schema': 1, 'target': f'ohos-{options.arch}', 'curl': manifest['curl'], 'boringssl': manifest['boringssl'], 'zlib': manifest['zlib'], - 'native_sdk': json.loads((sdk / 'oh-uni-package.json').read_text()), + 'native_sdk': sdk_metadata, }, indent=2) + '\n') if options.deps_only: return bridge = work / 'bridge' configure(cmake, ROOT / 'src', bridge, [*common, f'-DEH_DEPS_ROOT={stage}', f'-DEH_DART_INCLUDE={options.dart_sdk.resolve()}/include', - '-DEH_BUILD_SMOKE=ON']) + '-DEH_BUILD_SMOKE=ON'], identity) run([cmake, '--build', bridge, '--parallel', parallel]) print(f'OpenHarmony library: {bridge}/out/libech_http.so', flush=True) diff --git a/tool/test_build_ohos.py b/tool/test_build_ohos.py new file mode 100644 index 0000000..ac2f034 --- /dev/null +++ b/tool/test_build_ohos.py @@ -0,0 +1,74 @@ +"""Offline regression tests for the OHOS CMake cache lifecycle.""" +from pathlib import Path +import shutil +import subprocess +from tempfile import TemporaryDirectory +import unittest + +from build_ohos import configure + + +@unittest.skipUnless(shutil.which('cmake'), 'CMake is required') +class ConfigureTest(unittest.TestCase): + def setUp(self): + self.temporary = TemporaryDirectory(prefix='ech-ohos-cache-') + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.source = self.root / 'source-v1' + self.source.mkdir() + (self.source / 'CMakeLists.txt').write_text( + 'cmake_minimum_required(VERSION 3.22)\n' + 'project(cache_test NONE)\n' + 'set(CACHED_RESULT "${PIN}" CACHE STRING "configure probe")\n' + ) + self.build = self.root / 'build' + self.download = self.root / 'sources' / 'archive.tar' + self.download.parent.mkdir() + self.download.write_text('cached download') + + def check_configure(self, source=None, pin='one', identity=None): + configure('cmake', source or self.source, self.build, + [f'-DPIN={pin}'], identity) + self.assertEqual(self.download.read_text(), 'cached download') + + def test_unchanged_configuration_preserves_incremental_outputs(self): + self.check_configure() + artifact = self.build / 'incremental-output' + artifact.write_text('keep') + self.check_configure() + self.assertEqual(artifact.read_text(), 'keep') + + def test_dependency_source_can_move_to_a_new_revision(self): + self.check_configure() + source = self.root / 'source-v2' + shutil.copytree(self.source, source) + self.check_configure(source=source) + self.assertIn(f'CMAKE_HOME_DIRECTORY:INTERNAL={source}', + (self.build / 'CMakeCache.txt').read_text()) + + def test_changed_arguments_repeat_cached_configure_probes(self): + self.check_configure() + self.check_configure(pin='two') + self.assertIn('CACHED_RESULT:STRING=two', + (self.build / 'CMakeCache.txt').read_text()) + + def test_dependency_and_sdk_updates_discard_generated_outputs(self): + for identity in ({'dependency': 'v1', 'sdk': 'v1'}, + {'dependency': 'v2', 'sdk': 'v1'}, + {'dependency': 'v2', 'sdk': 'v2'}): + with self.subTest(identity=identity): + self.check_configure(identity=identity) + artifact = self.build / 'obsolete-output' + self.assertFalse(artifact.exists()) + artifact.write_text('remove on change') + + def test_migrates_a_build_without_a_fingerprint(self): + subprocess.run(['cmake', '-S', str(self.source), '-B', str(self.build), + '-DPIN=legacy'], check=True, capture_output=True) + self.check_configure() + self.assertIn('CACHED_RESULT:STRING=one', + (self.build / 'CMakeCache.txt').read_text()) + + +if __name__ == '__main__': + unittest.main() From 631479bf84a9e8e1105b93ea29b805213c4c0020 Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 05:19:44 +0800 Subject: [PATCH 4/8] test(ohos): verify HAP native assets and clarify cache documentation --- .github/workflows/ci.yml | 16 ++++++++++++++++ README.md | 6 +++++- README.zh-CN.md | 5 ++++- doc/ohos.zh-CN.md | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c196c8..fec82ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,22 @@ jobs: run: | flutter pub get flutter build hap --release --no-codesign + - name: Verify the HAP contains the native library and asset mapping + shell: bash + run: | + python3 - <<'PY' + import json + from pathlib import Path + from zipfile import ZipFile + haps = list(Path('build/flutter_smoke/build/ohos/hap').glob('*.hap')) + assert haps, 'No HAP was produced' + for path in haps: + with ZipFile(path) as hap: + assert 'libs/arm64-v8a/libech_http.so' in hap.namelist() + manifest = json.loads(hap.read('resources/rawfile/flutter_assets/NativeAssetsManifest.json')) + asset = manifest['native-assets']['ohos_arm64']['package:ech_http/ech_http_bindings'] + assert asset == ['absolute', 'libech_http.so'], asset + PY - uses: actions/upload-artifact@v4 with: name: ohos-native-assets diff --git a/README.md b/README.md index 72e4009..64a4555 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ final client = EchClient( ## Offline Builds & Binary Caching -By default, Dart build hooks download precompiled dependency SDKs on the first build and cache them under `.dart_tool`. For CI/CD environments or reproducible offline builds, configure a shared binary cache directory in your application's `pubspec.yaml`: +On Windows, Linux, Apple, and Android targets, Dart build hooks download precompiled dependency SDKs on the first build and cache them under `.dart_tool`. For CI/CD environments or reproducible offline builds, configure a shared binary cache directory in your application's `pubspec.yaml`: ```yaml hooks: @@ -209,6 +209,10 @@ hooks: - The build hook verifies the SHA-256 checksum of downloaded archives against `lib/src/build_support/dependencies.json`. - Cached archives are validated on each build; any damaged or modified extracted files are automatically repaired from the cached archive. +OHOS uses a source cache instead of precompiled SDKs. See the +[OHOS guide](doc/ohos.md#caching-and-dart-sdk-changes) for `source_cache`, +cache invalidation, and switching between Dart SDKs. + --- ## Operational Limits & Design Constraints diff --git a/README.zh-CN.md b/README.zh-CN.md index 3214061..81ae46d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -192,7 +192,7 @@ final client = EchClient( ## 离线构建与二进制缓存配置 -默认情况下,构建钩子会在首次构建时自动下载所需的依赖 SDK 并缓存在 `.dart_tool` 目录下。在 CI/CD 流水线或离线隔离环境中,可以在消费方项目的 `pubspec.yaml` 中指定持久化的共享缓存目录: +Windows、Linux、Apple 和 Android 目标的构建钩子会在首次构建时自动下载依赖 SDK 并缓存在 `.dart_tool` 目录下。在 CI/CD 流水线或离线隔离环境中,可以在消费方项目的 `pubspec.yaml` 中指定持久化的共享缓存目录: ```yaml hooks: @@ -205,6 +205,9 @@ hooks: - 构建钩子会依据 `lib/src/build_support/dependencies.json` 中的 SHA-256 散列严格核验下载档案的完整性。 - 每次构建都会校验解压文件;若本地解压文件损坏或被修改,钩子会自动基于缓存的压缩包进行自愈修复。 +OHOS 使用源码缓存,`source_cache`、缓存失效和 Dart SDK 切换说明见 +[鸿蒙指南](doc/ohos.zh-CN.md#缓存与工具链切换)。 + --- ## 运行指标与设计约束 diff --git a/doc/ohos.zh-CN.md b/doc/ohos.zh-CN.md index 36269ef..e18fd23 100644 --- a/doc/ohos.zh-CN.md +++ b/doc/ohos.zh-CN.md @@ -150,7 +150,7 @@ sni=encrypted Native Assets 集成和公网 HTTPS/ECH;gzip、代理、取消请求、isolate 退出 等行为已覆盖桌面回归测试,尚未在鸿蒙真机上逐项复测。 x64 仅完成交叉编译和构建钩子检查。初次本地真机验证未包含 Release HAP -与远程 CI;后续远程验证结果见下文记录。 +与远程 CI;远程检查覆盖范围见下文。 ## 缓存与工具链切换 From 0a94849d7e1efba33abafd3a2529cf8dca4a2268 Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 13:44:54 +0800 Subject: [PATCH 5/8] refactor(ohos): consume published precompiled dependency SDKs --- .github/workflows/ci.yml | 14 +- CHANGELOG.md | 8 +- README.md | 20 ++- README.zh-CN.md | 16 +-- doc/ohos.md | 123 ++++++++-------- doc/ohos.zh-CN.md | 101 ++++++------- doc/verification.md | 5 +- doc/verification.zh-CN.md | 5 +- hook/build.dart | 43 ++---- lib/src/build_support/dependencies.json | 10 ++ test/ohos_build_test.dart | 46 +++--- test/prebuilt_test.dart | 67 +++++++-- tool/build_ohos.py | 182 ------------------------ tool/ohos/CMakeLists.txt | 37 ----- tool/ohos/EchHttpDeps.cmake | 15 -- tool/test_build_ohos.py | 74 ---------- tool/update_prebuilt.py | 8 +- 17 files changed, 256 insertions(+), 518 deletions(-) delete mode 100644 tool/build_ohos.py delete mode 100644 tool/ohos/CMakeLists.txt delete mode 100644 tool/ohos/EchHttpDeps.cmake delete mode 100644 tool/test_build_ohos.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fec82ed..8cb17e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,8 +102,7 @@ jobs: version: 26.0.0.821 cache: true - run: dart pub get - - run: python3 tool/test_build_ohos.py - - run: dart test test/ohos_build_test.dart -r expanded + - run: dart test test/ohos_build_test.dart test/prebuilt_test.dart -r expanded - name: Cross-compile using the Flutter-OH compatibility protocol shell: bash run: | @@ -116,13 +115,24 @@ jobs: python3 - <<'PY' import json from pathlib import Path + import re import shutil + import struct + import subprocess for arch in ('arm64', 'x64'): manifest = Path(f'.dart_tool/verify/ohos-{arch}-device/output.json') data = json.loads(manifest.read_text()) assert data['status'] == 'success' assets = [a['encoding'] for a in data['assets']] library = next(a['file'] for a in assets if a['id'] == 'package:ech_http/ech_http_bindings') + elf = Path(library).read_bytes() + assert elf[:6] == b'\x7fELF\x02\x01' + assert struct.unpack_from('|"Verify SHA-256"| SDKCache["Precompiled SDK Cache\n(.dart_tool or user_defines)"] - SDKCache -->|"Fetch Target Only"| PrebuiltRepo["GitHub Releases\n(Windows / Linux / Apple / Android)"] + SDKCache -->|"Fetch Target Only"| PrebuiltRepo["GitHub Releases\n(Windows / Linux / Apple / Android / OHOS)"] Hook -->|"Compile C++ Bridge Only"| CMakeNinja["CMake + Ninja"] - Hook -->|"OHOS: Build Pinned Sources"| OhosSources["BoringSSL + curl + zlib"] - OhosSources --> CMakeNinja CMakeNinja -->|"Bundle Native Library"| OutputLib["ech_http.dll / .so / .dylib"] end ``` @@ -65,7 +63,7 @@ prompt cleanup. - **Authenticated ECH Retries**: Supports up to 2 automated ECH retries when the server responds with a valid TLS `retry_configs` rejection. Public names are strictly validated before retrying. - **Separation of Destination Routing and SNI**: IP address overrides (`addresses`) redirect only the underlying TCP socket or proxy CONNECT tunnel. The TLS inner SNI, certificate identity verification, and HTTP `Host` header always correspond to the original requested hostname. - **Strict TLS Verification**: Every connection strictly validates the server's certificate chain and hostname against the bundled Mozilla CA root certificates. There is **no insecure mode** or certificate bypass switch. Custom PKI roots can be explicitly loaded via `trustedRootsPem`. -- **Automatic Native Builds**: Build hooks (`hook/build.dart`) download verified precompiled dependency SDKs for Windows, Linux, Apple, and Android, compiling only the lightweight C++ bridge locally (~15 s cold build). OHOS builds pinned BoringSSL, curl, and zlib sources on the first build and reuses them incrementally. +- **Automatic Native Builds**: Build hooks (`hook/build.dart`) download verified precompiled dependency SDKs for every supported native platform, including OHOS, and compile only the lightweight C++ bridge locally. BoringSSL, curl, and zlib are built and released by independent dependency repositories. - **Zero Runtime Daemons**: Communicates directly through Dart FFI within the host process memory space, avoiding child processes, socket proxy bottlenecks, and OS port conflicts. --- @@ -79,10 +77,10 @@ prompt cleanup. | **macOS** | `x64`, `arm64` | macOS host, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (device), `arm64`/`x64` (simulator) | macOS host, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (or Flutter bundled NDK), CMake, Ninja | Android API level 21+ (5.0 Lollipop) | -| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, Python 3.12+, Git, Go, CMake, Ninja; Flutter-OH Native Assets for HAPs | See [OHOS validation](doc/ohos.md) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, CMake, Ninja; Flutter-OH Native Assets for HAPs | SDK API 26; see [OHOS validation](doc/ohos.md) | > [!NOTE] -> Web (Browsers) is not supported. OHOS builds its pinned dependencies from source on the first build; the other platforms use prebuilt SDKs. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. +> Web (Browsers) is not supported. All native platforms, including OHOS, use versioned dependency SDKs pinned by SHA-256. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. --- @@ -196,7 +194,7 @@ final client = EchClient( ## Offline Builds & Binary Caching -On Windows, Linux, Apple, and Android targets, Dart build hooks download precompiled dependency SDKs on the first build and cache them under `.dart_tool`. For CI/CD environments or reproducible offline builds, configure a shared binary cache directory in your application's `pubspec.yaml`: +On all supported native targets, Dart build hooks download precompiled dependency SDKs on the first build and cache them under `.dart_tool`. For CI/CD environments or reproducible offline builds, configure a shared binary cache directory in your application's `pubspec.yaml`: ```yaml hooks: @@ -209,9 +207,9 @@ hooks: - The build hook verifies the SHA-256 checksum of downloaded archives against `lib/src/build_support/dependencies.json`. - Cached archives are validated on each build; any damaged or modified extracted files are automatically repaired from the cached archive. -OHOS uses a source cache instead of precompiled SDKs. See the -[OHOS guide](doc/ohos.md#caching-and-dart-sdk-changes) for `source_cache`, -cache invalidation, and switching between Dart SDKs. +OHOS uses the same `binary_cache` mechanism. See the +[OHOS guide](doc/ohos.md#caching-and-dart-sdk-changes) for SDK baselines, +cache behavior, and switching between Dart SDKs. --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 81ae46d..a434924 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,7 +9,7 @@ [English](README.md) `ech_http` 支持 [OpenHarmony / HarmonyOS 构建](doc/ohos.zh-CN.md), -通过 Flutter-OH Native Assets 从固定版本源码构建原生依赖,详见接入与验证说明。 +通过 Flutter-OH Native Assets 下载经过校验的预编译 SDK,本地仅编译胶水代码,详见接入与验证说明。 `ech_http` 是专为 Dart 和 Flutter 打造的企业级 HTTP 客户端,深度集成了 **TLS 密文客户端问候(Encrypted Client Hello, ECH)**、HTTP CONNECT 代理、自定义 PKI 信任根及流式数据传输功能。完全兼容官方 `package:http.Client` 接口规范。 @@ -37,10 +37,8 @@ flowchart TD subgraph BuildSystem["Dart Build Hook (构建期)"] Hook["hook/build.dart 构建钩子"] -->|"校验 SHA-256 摘要"| SDKCache["预编译 SDK 本地缓存\n(.dart_tool 或 user_defines)"] - SDKCache -->|"仅下载当前构建目标"| PrebuiltRepo["GitHub Releases\n(Windows / Linux / Apple / Android)"] + SDKCache -->|"仅下载当前构建目标"| PrebuiltRepo["GitHub Releases\n(Windows / Linux / Apple / Android / OHOS)"] Hook -->|"仅就地编译 C++ 桥接代码"| CMakeNinja["CMake + Ninja"] - Hook -->|"OHOS:编译固定版本源码"| OhosSources["BoringSSL + curl + zlib"] - OhosSources --> CMakeNinja CMakeNinja -->|"打包原生动态库"| OutputLib["ech_http.dll / .so / .dylib"] end ``` @@ -61,7 +59,7 @@ flowchart TD - **原生支持认证重试(Authenticated Retry)**:当服务端返回合法的 TLS `retry_configs`(配置拒绝重试)时,客户端会自动进行最多 2 次认证重试。重试前会严格校验外层公用名(Public Name)以防篡改。 - **物理路由与证书身份严格解耦**:自定义目标 IP 地址列表(`addresses`)仅重定向底层的 TCP Socket 物理连接或代理 CONNECT 隧道;TLS 握手中的内层 SNI、服务端证书主体身份核验以及 HTTP `Host` 请求头始终保持原始目标域名不变。 - **严格证书校验,无旁路后门**:所有连接默认基于内置的 Mozilla CA 根证书库严格验证证书链与主机名。**库中未设置任何跳过证书验证的不安全开关**。如需使用企业私有 PKI,可通过 `trustedRootsPem` 显式替换信任根。 -- **自动原生构建**:借助 Dart `hook/build.dart`,Windows、Linux、Apple 和 Android 按需拉取经过校验的预编译 SDK,本地仅编译轻量级 C++ 桥接代码(冷构建约 15 秒)。OHOS 首次构建编译固定版本的 BoringSSL、curl 和 zlib,后续增量复用产物。 +- **自动原生构建**:借助 Dart `hook/build.dart`,各原生平台均按需拉取经过校验的预编译 SDK,本地仅编译轻量级 C++ 桥接代码。BoringSSL、curl 和 zlib 由独立的依赖仓库提前编译、发布。 - **零外部守护进程**:直接在应用主进程内存中通过 Dart FFI 调用原生接口,避免了子进程开销、本地端口占用冲突及系统级代理注入风险。 --- @@ -75,10 +73,10 @@ flowchart TD | **macOS** | `x64`, `arm64` | macOS 系统, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (真机), `arm64`/`x64` (模拟器) | macOS 系统, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (或 Flutter 自带 NDK), CMake, Ninja | Android API Level 21+ (5.0 Lollipop) | -| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK、Python 3.12+、Git、Go、CMake、Ninja;HAP 需要 Flutter-OH Native Assets | 见[鸿蒙验证说明](doc/ohos.zh-CN.md) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK、CMake、Ninja;HAP 需要 Flutter-OH Native Assets | SDK API 26;见[鸿蒙验证说明](doc/ohos.zh-CN.md) | > [!NOTE] -> 当前不支持浏览器 Web 环境。鸿蒙首次构建从固定版本源码编译依赖,其他平台使用预编译 SDK。Android 原生库使用静态 libc++ 链接,并满足 Android 15 强制要求的 16 KiB ELF 页面对齐。 +> 当前不支持浏览器 Web 环境。包括鸿蒙在内的原生平台均使用固定版本、SHA-256 校验的预编译依赖 SDK。Android 原生库使用静态 libc++ 链接,并满足 Android 15 强制要求的 16 KiB ELF 页面对齐。 --- @@ -192,7 +190,7 @@ final client = EchClient( ## 离线构建与二进制缓存配置 -Windows、Linux、Apple 和 Android 目标的构建钩子会在首次构建时自动下载依赖 SDK 并缓存在 `.dart_tool` 目录下。在 CI/CD 流水线或离线隔离环境中,可以在消费方项目的 `pubspec.yaml` 中指定持久化的共享缓存目录: +各原生目标的构建钩子会在首次构建时自动下载依赖 SDK 并缓存在 `.dart_tool` 目录下。在 CI/CD 流水线或离线隔离环境中,可以在消费方项目的 `pubspec.yaml` 中指定持久化的共享缓存目录: ```yaml hooks: @@ -205,7 +203,7 @@ hooks: - 构建钩子会依据 `lib/src/build_support/dependencies.json` 中的 SHA-256 散列严格核验下载档案的完整性。 - 每次构建都会校验解压文件;若本地解压文件损坏或被修改,钩子会自动基于缓存的压缩包进行自愈修复。 -OHOS 使用源码缓存,`source_cache`、缓存失效和 Dart SDK 切换说明见 +OHOS 同样使用 `binary_cache`;SDK 版本、缓存和 Dart SDK 切换说明见 [鸿蒙指南](doc/ohos.zh-CN.md#缓存与工具链切换)。 --- diff --git a/doc/ohos.md b/doc/ohos.md index 3ba082d..14cb74f 100644 --- a/doc/ohos.md +++ b/doc/ohos.md @@ -9,8 +9,11 @@ verification scope below for the checks performed on each architecture. ## Requirements -- Dart >=3.10, Python >=3.12, Git, CMake >=3.22, Ninja, and Go (for BoringSSL). -- An OpenHarmony native SDK containing `build/cmake/ohos.toolchain.cmake`. +- Dart >=3.10, CMake >=3.22, and Ninja. `ech_http` consumers do not need Python + or Go to build the precompiled dependencies. +- An OpenHarmony / HarmonyOS native SDK containing `build/cmake/ohos.toolchain.cmake`. + The precompiled SDK and verified consumer use version 26.0.0.105 / API 26; + compatibility with lower API levels has not been verified. `OHOS_NDK_HOME` may point to the native directory or its parent. The HarmonyOS command-line tools layout `HOS_SDK_HOME/default/openharmony/native` is also supported. @@ -52,57 +55,49 @@ hooks: ech_http: target: ohos ohos_sdk: /absolute/path/to/openharmony/native - # Optional: share downloaded pinned sources between builds. - source_cache: /absolute/path/to/ech-http-sources + # Optional: share verified precompiled SDKs between builds. + binary_cache: /absolute/path/to/ech-http-sdks ``` -The first build downloads and compiles the BoringSSL, curl, and zlib versions -pinned in `lib/src/build_support/dependencies.json`. curl and zlib archives -are checked against SHA-256 digests; BoringSSL is checked against its Git -revision. Later builds reuse Ninja outputs incrementally. No additional -prebuilt dependency repository is required. +The first build downloads the precompiled SDK pinned by version and SHA-256 +in `lib/src/build_support/dependencies.json`. After validating metadata and +per-file hashes, the hook compiles only `src/ech_http.cpp`. Consumers no longer +download or compile BoringSSL, curl, or zlib sources. Later builds reuse the +verified SDK cache, with the same integrity and repair checks as other platforms. -OHOS links the C++ runtime statically and hides static library symbols. The -HAP does not need an extra `libc++_shared.so`, and the TLS library's symbols -are not exported to other native plugins. +The independent [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) +repository builds and publishes these SDKs, starting with +[v0.1.0](https://github.com/tignioj/libechhttp_ohos_build/releases/tag/v0.1.0). +Each archive includes matching headers, four static libraries, CMake targets, +licenses, per-file hashes, and build provenance. Maintainers update reviewed +pins with `python3 tool/update_prebuilt.py ohos=v0.1.0`; application hooks never +invoke that Python tool. + +OHOS links the C++ runtime statically and hides static library symbols. The HAP +does not need an extra `libc++_shared.so`, and TLS symbols are not exported to +other native plugins. ## Standalone builds -Check cross-compilation and Native Assets output without creating an app: +Check SDK download, bridge cross-compilation, and Native Assets output without +creating an application: ```sh export OHOS_NDK_HOME=/absolute/path/to/openharmony/native dart pub get dart run tool/check_native_build.dart ohos arm64 +dart run tool/check_native_build.dart ohos x64 ``` -The check uses Flutter-OH's Linux compatibility input and OHOS compiler. -The manifest at `.dart_tool/verify/ohos-arm64-device/output.json` identifies -the OHOS library as `package:ech_http/ech_http_bindings`. -Repeat with `ohos x64` to verify the other supported architecture. - -To build the library and native request smoke executable directly: +The checks use Flutter-OH's Linux compatibility input and OHOS compiler, and +download the corresponding precompiled SDK. Manifests are written to +`.dart_tool/verify/ohos-arm64-device/output.json` and +`.dart_tool/verify/ohos-x64-device/output.json`, with the library identified as +`package:ech_http/ech_http_bindings`. -```sh -python3 tool/build_ohos.py \ - --native-sdk "$OHOS_NDK_HOME" \ - --dart-sdk /absolute/path/to/dart-sdk \ - --arch arm64 -``` - -Outputs are in `build/ohos/arm64/bridge/out/`. `--arch x64` uses -`build/ohos/x64/`. Flutter-OH builds run the same dependency build steps. - -The native smoke executable accepts four arguments: - -```sh -./ech_http_smoke URL HTTP_PROXY_OR_EMPTY ECH_CONFIG_LIST_OR_EMPTY TARGET_IP_OR_EMPTY -``` - -With a nonempty ECH configuration, success requires HTTP 200 and accepted ECH. -Commercial HarmonyOS devices may reject unsigned ELF executables. Use an -official debug signature or a normally signed test HAP; executable file -permissions do not replace code signing. +Maintainers rebuilding dependencies use the independent SDK repository's build +and publication workflow. Neither this package nor consumers contain an entry +point to compile those dependency sources. ## Flutter device smoke app @@ -127,7 +122,7 @@ builds do not use that AOT compiler. ## Verification scope -Local verification on 2026-10-17 used a macOS arm64 host, Dart 3.13.5, +The initial source-build verification on 2026-10-17 used a macOS arm64 host, Dart 3.13.5, and OpenHarmony native SDK 6.0.0.47 (API 20). The Flutter HAP used Flutter-OH 3.41.9+ohos-1.0.2, its bundled Dart 3.11.5, and HarmonyOS SDK 26.0.0.105. The connected MatePad mini runs HarmonyOS API 26. @@ -144,10 +139,10 @@ Flutter-OH 3.41.9+ohos-1.0.2, its bundled Dart 3.11.5, and HarmonyOS SDK | Debug HAP packaging | Passed with arm64 `libech_http.so` and Native Assets mapping | | Local Release HAP build | Native library and Dart compilation passed; AOT requires Rosetta, absent on the verified host | | Bare ELF execution on MatePad mini | Rejected with `Permission denied` before request code ran | -| Signed Debug HAP on MatePad mini | Native library loaded and FFI symbols resolved | -| Device HTTPS / ECH request | HTTP 200, accepted ECH, `sni=encrypted`, and `tls=TLSv1.3` | +| Initial source-build signed Debug HAP on MatePad mini | Native library loaded and FFI symbols resolved | +| Initial source-build device HTTPS / ECH request | HTTP 200, accepted ECH, `sni=encrypted`, and `tls=TLSv1.3` | -The signed HAP's device log contains: +The earlier source-build signed HAP's device log contains: ```text ECH_HTTP_SMOKE: libcurl/8.22.0 BoringSSL zlib/1.3.2 @@ -162,18 +157,27 @@ file, not part of the package). Device verification covers Native Assets integration and public HTTPS/ECH. gzip, proxies, cancellation, and isolate shutdown have desktop regression coverage but have not each been repeated on an OHOS device. x64 has build coverage only. Release HAP and remote CI -verification were not part of the initial local device run. +verification were not part of the initial local device run. Those historical +device results do not mean the precompiled SDK version has been rerun on a device. + +The dependency repository's [initial SDK CI](https://github.com/tignioj/libechhttp_ohos_build/actions/runs/39006660450) +verified arm64/x64 dependency and packaged consumer links, ELF architecture, +dynamic dependencies, archive and file digests, and source provenance. +It does not execute target device code. + +One local macOS arm64 check on the same date downloaded this Release into an +empty SDK cache and built the bridge in 6.7 seconds for arm64 and 7.0 seconds +for x64. These observations include downloading and building, depend on the +network and build host, and are not a timing guarantee for other environments. ## Caching and Dart SDK changes -Build directories are separate for each architecture. The source builder -records each dependency and standalone bridge configuration's source path, configure -arguments, dependency pins, native SDK metadata, and toolchain digest. -Unchanged inputs preserve incremental outputs. Changed inputs discard the -generated directory and repeat configuration probes and compilation. This -also migrates older build directories without a configuration record. -Downloaded sources are stored separately and survive this invalidation. -The build hook also resets bridge CMake metadata when the package source moves. +OHOS shares `preparePrebuilt` with the other platforms. The target and archive +SHA-256 identify the cache directory. ZIP digests, SDK metadata, and per-file +hashes are checked; modified extracted files are repaired from the verified +archive. New archive pins use a new cache directory. `binary_cache` paths are +relative to the consuming `pubspec.yaml`; a complete cache works offline. +The hook resets bridge CMake metadata when the package source moves. When switching between standard Dart and Flutter-OH's bundled Dart, regenerate the application's `.dart_tool` and run `flutter pub get`. @@ -181,12 +185,15 @@ Otherwise a cached `hook.dill` may produce `Invalid kernel binary format version`. CI keeps package tools on their original Dart SDK and gives the Flutter-OH consumer its own `.dart_tool`. -Run the cache regression checks without networking or an OHOS SDK: - ```sh -python3 tool/test_build_ohos.py +dart test test/ohos_build_test.dart test/prebuilt_test.dart ``` -The OHOS CI job checks SDK detection, cache invalidation, arm64/x64 hook -outputs, and an unsigned Release HAP built with the pinned Flutter-OH -toolchain. Remote packaging checks do not replace signed device execution. +Independent SDK CI builds and link-checks both architectures, validates ELF +properties, and verifies complete archives. Package CI downloads published +SDKs, builds only the bridge, and checks arm64/x64 hook outputs plus unsigned +Release HAP contents and Native Assets mapping with pinned Flutter-OH. +Neither pipeline replaces signed device execution. x64 still has no device +coverage. Proxy, gzip, cancellation, and isolate shutdown have desktop tests +but have not each been repeated on an OHOS device. Rerunning the precompiled +SDK version on a device is outside these automated build checks. diff --git a/doc/ohos.zh-CN.md b/doc/ohos.zh-CN.md index e18fd23..3d9a223 100644 --- a/doc/ohos.zh-CN.md +++ b/doc/ohos.zh-CN.md @@ -8,8 +8,9 @@ ## 构建要求 -- Dart >=3.10、Python >=3.12、Git、CMake >=3.22、Ninja、Go(编译 BoringSSL 所需)。 -- OpenHarmony native SDK,包含 `build/cmake/ohos.toolchain.cmake`。 +- Dart >=3.10、CMake >=3.22、Ninja;依赖库已预编译,无需为 `ech_http` 安装 Python 或 Go。 +- OpenHarmony / HarmonyOS native SDK,包含 `build/cmake/ohos.toolchain.cmake`。 + 预编译 SDK 使用 26.0.0.105 / API 26;当前消费方也在该版本验证,低 API 兼容性未验证。 可将 `OHOS_NDK_HOME` 指向 `native` 本身或其父目录;也支持 HarmonyOS 命令行工具的 `HOS_SDK_HOME/default/openharmony/native` 布局。 - 应用构建需要已支持 OHOS Native Assets 的 Flutter-OH。 @@ -47,51 +48,43 @@ hooks: ech_http: target: ohos ohos_sdk: /absolute/path/to/openharmony/native - # 可选:跨构建复用已下载的固定版本源码。 - source_cache: /absolute/path/to/ech-http-sources + # 可选:跨构建复用经过校验的预编译 SDK。 + binary_cache: /absolute/path/to/ech-http-sdks ``` -首次构建会从包内 `dependencies.json` 指定的版本下载和编译 BoringSSL、 -curl、zlib。curl/zlib 校验 SHA-256,BoringSSL 校验 Git 提交。 -随后 Ninja 增量复用编译结果。无需另外创建或发布预编译依赖仓库。 -OHOS 使用静态 C++ 运行库,并隐藏其符号;HAP 不必额外打包 +首次构建下载 `lib/src/build_support/dependencies.json` 固定版本与 SHA-256 的 +预编译依赖 SDK,校验元数据及逐文件摘要后,本地仅编译 `src/ech_http.cpp` +胶水代码。消费方不再下载、编译 BoringSSL、curl、zlib 源码。 +后续构建复用已校验的 SDK 缓存;下载与缓存修复机制和其他平台一致。 + +预编译 SDK 由独立仓库 +[libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) 构建和发布, +首版为 [v0.1.0](https://github.com/tignioj/libechhttp_ohos_build/releases/tag/v0.1.0)。 +包含匹配头文件、四个静态库、CMake 配置、许可证和构建来源元数据。 +维护者更新 SDK 时运行 `python3 tool/update_prebuilt.py ohos=v0.1.0`; +该 Python 工具不会由应用构建钩子调用。 + +OHOS 使用静态 C++ 运行库,并隐藏静态依赖符号。HAP 不必额外打包 `libc++_shared.so`,也不会把 TLS 库的符号暴露给其他播放器插件。 ## 独立编译与检查 -不必先创建 Flutter 应用即可检查交叉编译和 Native Assets 输出: +不必先创建 Flutter 应用即可检查 SDK 下载、胶水代码交叉编译和 Native Assets 输出: ```sh export OHOS_NDK_HOME=/absolute/path/to/openharmony/native dart pub get dart run tool/check_native_build.dart ohos arm64 +dart run tool/check_native_build.dart ohos x64 ``` -该检查使用与 Flutter-OH 一致的 Linux 兼容输入和 OHOS 编译器。 -清单位于 `.dart_tool/verify/ohos-arm64-device/output.json`,包含 +该检查使用与 Flutter-OH 一致的 Linux 兼容输入和 OHOS 编译器,下载对应架构的 +预编译 SDK。清单位于 `.dart_tool/verify/ohos-arm64-device/output.json` +或 `.dart_tool/verify/ohos-x64-device/output.json`,包含 `package:ech_http/ech_http_bindings` 对应的鸿蒙 `.so`。 -也可单独生成库与原生请求测试程序: - -```sh -python3 tool/build_ohos.py \ - --native-sdk "$OHOS_NDK_HOME" \ - --dart-sdk /absolute/path/to/dart-sdk \ - --arch arm64 -``` - -产物位于 `build/ohos/arm64/bridge/out/`。`--arch x64` 使用独立的 -`build/ohos/x64/` 目录。已有 Flutter-OH 的构建会自动运行相同的依赖编译步骤。 - -原生测试程序参数与现有工具一致: - -```sh -./ech_http_smoke URL HTTP代理或空字符串 ECHConfigList或空字符串 目标IP或空字符串 -``` - -ECH 配置非空时,测试要求响应为 HTTP 200 且报告 ECH 被服务器接受。 -HarmonyOS 商用设备可能拒绝直接执行未签名 ELF;这种情况下应使用官方 -调试签名,或将库放入正常签名的测试 HAP。普通文件执行权限并不能替代代码签名。 +维护者如需重建大型依赖,应在独立 SDK 仓库执行构建与发布流程; +应用项目和本包内不再提供源码依赖编译入口。 ## Flutter 真机测试包 @@ -116,7 +109,7 @@ Debug 测试包不依赖这个 AOT 编译器。 ## 验证范围 -2026-10-17 本地检查使用 macOS arm64 构建机、Dart 3.13.5、OpenHarmony +2026-10-17 初始源码构建版本的本地检查使用 macOS arm64 构建机、Dart 3.13.5、OpenHarmony native SDK 6.0.0.47(API 20)。Flutter HAP 使用 Flutter-OH 3.41.9+ohos-1.0.2、其内置 Dart 3.11.5 和 HarmonyOS SDK 26.0.0.105。 连接的 MatePad mini 为 HarmonyOS API 26。 @@ -133,10 +126,10 @@ native SDK 6.0.0.47(API 20)。Flutter HAP 使用 Flutter-OH | Debug HAP 构建及库打包 | 通过,包内包含 arm64 `libech_http.so` 与 Native Assets 映射 | | 本机 Release HAP 构建 | 原生库与 Dart 编译通过;AOT 编译器需要本机尚未安装的 Rosetta | | MatePad mini 上执行裸 ELF | 系统返回 `Permission denied`,未执行到请求代码 | -| 已签名 Flutter Debug HAP 真机运行 | 通过,Native Assets 中的 arm64 库正常加载并解析 FFI 符号 | -| MatePad mini HTTPS / ECH 请求 | 通过,DoH 获取 ECH 配置后返回 HTTP 200、`ECH accepted: true`,服务端返回 `sni=encrypted`、`tls=TLSv1.3` | +| 初始源码构建版本的签名 Debug HAP 真机运行 | 通过,Native Assets 中的 arm64 库正常加载并解析 FFI 符号 | +| 初始源码构建版本的 MatePad mini HTTPS / ECH 请求 | 通过,DoH 获取 ECH 配置后返回 HTTP 200、`ECH accepted: true`,服务端返回 `sni=encrypted`、`tls=TLSv1.3` | -真机日志中的关键结果如下(来自正常签名安装的 Flutter HAP): +此前源码构建版本的真机日志如下(来自正常签名安装的 Flutter HAP): ```text ECH_HTTP_SMOKE: libcurl/8.22.0 BoringSSL zlib/1.3.2 @@ -150,29 +143,37 @@ sni=encrypted Native Assets 集成和公网 HTTPS/ECH;gzip、代理、取消请求、isolate 退出 等行为已覆盖桌面回归测试,尚未在鸿蒙真机上逐项复测。 x64 仅完成交叉编译和构建钩子检查。初次本地真机验证未包含 Release HAP -与远程 CI;远程检查覆盖范围见下文。 +与远程 CI。改为预编译 SDK 后,应以本次 SDK CI 与消费方 CI 为准; +此前真机记录不等同于已在设备上复测预编译 SDK 版本。 + +预编译依赖仓库的[首版 CI](https://github.com/tignioj/libechhttp_ohos_build/actions/runs/39006660450) +已验证 arm64、x64 的依赖与独立消费方链接、ELF 架构、动态依赖、归档摘要、 +逐文件摘要及来源提交。该 CI 不执行目标设备代码。 + +同日 macOS arm64 本地的一次检查中,从空的 SDK 缓存下载该 Release 并构建胶水代码, +arm64 耗时 6.7 秒、x64 耗时 7.0 秒。该观察包含下载和构建,受网络与构建机影响, +不代表所有环境的固定耗时。 ## 缓存与工具链切换 -构建目录按架构区分。源码构建脚本为每个依赖和独立编译的桥接层记录源码绝对路径、 -配置参数、BoringSSL/curl/zlib 版本与摘要、SDK 元数据及 toolchain 摘要。 -这些输入不变时复用 Ninja 增量产物;输入改变时清理对应的生成目录, -重新运行配置探测和编译,避免依赖升级后出现源码目录冲突或使用旧探测结果。 -迁移没有配置记录的旧构建目录时也会重新生成。 -已下载的源码独立保存,不受 CMake 构建目录清理影响。 -构建钩子的桥接层也会在包源码路径变更时重置 CMake 元数据。 +OHOS 和其他平台共用 `preparePrebuilt`:以目标架构和归档 SHA-256 区分缓存, +核验 ZIP、SDK 元数据及逐文件摘要;解压文件损坏时从已校验的归档修复。 +更换版本或归档摘要后自动使用新的缓存目录,不会把旧依赖产物混入新 SDK。 +`binary_cache` 路径相对于消费方 `pubspec.yaml`。缓存完整时可离线构建。 +构建钩子的胶水代码会在包源码路径变更时重置 CMake 元数据。 切换标准 Dart 与 Flutter-OH 内置 Dart 时,应重新生成应用的 `.dart_tool` 并运行 `flutter pub get`,避免旧 SDK 编译的 `hook.dill` 导致 `Invalid kernel binary format version`。CI 中包级检查始终使用原 Dart SDK, Flutter-OH 测试应用使用自身独立的 `.dart_tool` 和内置 Dart。 -运行无需网络或 OHOS SDK 的缓存回归检查: - ```sh -python3 tool/test_build_ohos.py +dart test test/ohos_build_test.dart test/prebuilt_test.dart ``` -CI 的 OHOS 作业检查 SDK 识别、缓存失效、arm64/x64 构建钩子输出, -以及固定 Flutter-OH 工具链的未签名 Release HAP 打包。远程构建不能 -替代签名 HAP 的真机运行检查。 +独立 SDK CI 验证两个架构的大型依赖编译、链接、ELF 和归档完整性; +本包 CI 从已发布 Release 下载 SDK,仅编译胶水代码,并检查 arm64/x64 +构建钩子输出及固定 Flutter-OH 工具链的未签名 Release HAP 打包和映射。 +两类 CI 都不替代签名 HAP 的真机运行。x64 尚未进行真机验证;代理、gzip、 +取消请求和 isolate 退出尚未在鸿蒙真机逐项复测。预编译 SDK 版本的设备复测 +不包含在本次自动化构建验证中。 diff --git a/doc/verification.md b/doc/verification.md index 95667aa..47d6cee 100644 --- a/doc/verification.md +++ b/doc/verification.md @@ -42,14 +42,14 @@ We distinguish between three tiers of verification: | **Android** | `arm64-v8a` | **Tier 1** | Physical device run (API 35), live ECH handshake, authenticated retry, connection reuse | Non-standard custom Android ROMs | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK packaging for both ABIs | Runtime execution on physical 32-bit ARM / x86_64 devices | | **Android** | `x86` | **Tier 2** | NDK bridge build and link check in CI | 32-bit x86 Android runtime execution | -| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | Both architectures cross-compile through the build hook; a signed arm64 Flutter Debug HAP returns HTTP 200 with ECH accepted on MatePad mini | See the [OHOS guide](ohos.md) for runtime scope; full suite not repeated on OHOS, no x64 device attached | +| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | arm64/x64 precompiled SDKs and bridge builds; an earlier source-build signed Debug HAP returned HTTP 200 with accepted ECH on MatePad mini | See the [OHOS guide](ohos.md) for runtime scope; precompiled SDK device rerun pending; full suite not repeated on OHOS, no x64 device attached | | **Web** | All | **Tier 3** | Not supported by this native C++ FFI architecture | N/A | --- ## 2. Precompiled SDK Release Registry -Precompiled dependency SDKs (statically built libcurl 8.22.0 + BoringSSL + zlib 1.3.2) are hosted in four dedicated public repositories: +Precompiled dependency SDKs (statically built libcurl 8.22.0 + BoringSSL + zlib 1.3.2) are hosted in five dedicated public repositories: | Build Repository | Release Tag | Target Platforms & Architectures | | :--- | :--- | :--- | @@ -57,6 +57,7 @@ Precompiled dependency SDKs (statically built libcurl 8.22.0 + BoringSSL + zlib | [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build) | `v0.1.2` | macOS / iOS: `x64`, `arm64` (Device + Simulator) | | [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build) | `v0.1.1` | Android: `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | | [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build) | `v0.1.1` | Linux: `x64`, `arm64` | +| [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) | `v0.1.0` | OpenHarmony / HarmonyOS: `arm64`, `x64` (SDK API 26) | All release archives and their SHA-256 digests are pinned in [lib/src/build_support/dependencies.json](../lib/src/build_support/dependencies.json). diff --git a/doc/verification.zh-CN.md b/doc/verification.zh-CN.md index 695f8a4..95de8ef 100644 --- a/doc/verification.zh-CN.md +++ b/doc/verification.zh-CN.md @@ -37,14 +37,14 @@ Flutter Android APK、iOS 未签名 Release 应用、iOS 模拟器 Debug 应用 | **Android** | `arm64-v8a` | **Tier 1** | 真实设备运行(API 35),公网 ECH 握手、认证重试、连接复用均通过 | 各类深度定制的非标准第三方 Android ROM | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK 多 ABI 打包测试通过 | 32 位 ARM / x86_64 物理机运行时执行 | | **Android** | `x86` | **Tier 2** | CI 中 NDK 桥接构建与链接核验通过 | 32 位 x86 Android 实体机运行时执行 | -| **OpenHarmony / HarmonyOS** | arm64、x64 | **Tier 2** | 两种架构交叉编译与构建钩子通过;arm64 签名 Flutter Debug HAP 在 MatePad mini 返回 HTTP 200、ECH 被接受 | 真机验证范围见[鸿蒙说明](ohos.zh-CN.md);完整功能套件未在鸿蒙复测,未连接 x64 设备 | +| **OpenHarmony / HarmonyOS** | arm64、x64 | **Tier 2** | arm64/x64 预编译 SDK 与胶水代码构建;此前源码构建版本的签名 Debug HAP 在 MatePad mini 返回 HTTP 200、ECH 被接受 | 真机验证范围见[鸿蒙说明](ohos.zh-CN.md);预编译 SDK 版本尚未复测签名真机;完整功能套件未在鸿蒙复测,未连接 x64 设备 | | **Web** | 全部架构 | **Tier 3** | C++ FFI 原生引擎架构不支持 Web | 不适用 | --- ## 2. 预编译 SDK 仓库索引 -预编译依赖库(静态构建的 libcurl 8.22.0、BoringSSL 与 zlib 1.3.2)托管在四个独立的公开构建仓库中: +预编译依赖库(静态构建的 libcurl 8.22.0、BoringSSL 与 zlib 1.3.2)托管在五个独立的公开构建仓库中: | 构建仓库 | 发布 Tag | 支持的目标平台与架构 | | :--- | :--- | :--- | @@ -52,6 +52,7 @@ Flutter Android APK、iOS 未签名 Release 应用、iOS 模拟器 Debug 应用 | [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build) | `v0.1.2` | macOS / iOS: `x64`, `arm64`(真机 + 模拟器) | | [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build) | `v0.1.1` | Android: `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | | [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build) | `v0.1.1` | Linux: `x64`, `arm64` | +| [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) | `v0.1.0` | OpenHarmony / HarmonyOS: `arm64`, `x64` (SDK API 26) | 每个 SDK 压缩包的 SHA-256 散列均固化在 [lib/src/build_support/dependencies.json](../lib/src/build_support/dependencies.json) 中。 diff --git a/hook/build.dart b/hook/build.dart index cbcd96b..de7f4b8 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -71,43 +71,16 @@ Future main(List args) async { iosSdk: code.targetOS == OS.iOS ? code.iOS.targetSdk : null, ) : 'ohos-${code.targetArchitecture.name}'; - final String sdk; - if (ohos == null) { - final dependency = PrebuiltDependency.fromManifest(manifest, target); - sdk = await preparePrebuilt(dependency, cache); - } else { - final python = - _onPath('python3', Platform.environment) ?? - _onPath('python', Platform.environment); - if (python == null) { - throw StateError('OHOS source builds require Python >=3.12'); - } - final builder = p.join(root, 'tool', 'build_ohos.py'); + if (ohos != null) { output.dependencies.addAll([ - Uri.file(builder), - Uri.file(p.join(root, 'tool', 'ohos', 'CMakeLists.txt')), - Uri.file(p.join(root, 'tool', 'ohos', 'EchHttpDeps.cmake')), Uri.file(p.join(ohos.nativeSdk, 'oh-uni-package.json')), + Uri.file( + p.join(ohos.nativeSdk, 'build', 'cmake', 'ohos.toolchain.cmake'), + ), ]); - final dependencyBuild = p.join(shared, target); - stderr.writeln( - 'ech_http: building pinned OHOS dependencies (cached after the first build)', - ); - await runner.run(python, [ - builder, - '--deps-only', - '--native-sdk', - ohos.nativeSdk, - '--arch', - code.targetArchitecture.name, - '--output', - dependencyBuild, - '--source-cache', - input.userDefines.path('source_cache')?.toFilePath() ?? - p.join(dependencyBuild, 'sources'), - ]); - sdk = p.join(dependencyBuild, 'sdk'); } + final dependency = PrebuiltDependency.fromManifest(manifest, target); + final sdk = await preparePrebuilt(dependency, cache); final tools = await _toolchain(code, runner, ohos: ohos); runner.environment = tools.environment; @@ -132,7 +105,9 @@ Future main(List args) async { } final nativeBuild = p.join(out, 'native'); await prepareCmakeDirectory(p.join(root, 'src'), nativeBuild); - stderr.writeln('ech_http: compiling C++ bridge with $target dependencies'); + stderr.writeln( + 'ech_http: compiling C++ bridge with prebuilt $target dependencies', + ); await runner.run(tools.cmake, [ '-S', p.join(root, 'src'), diff --git a/lib/src/build_support/dependencies.json b/lib/src/build_support/dependencies.json index 269ca02..18ca26d 100644 --- a/lib/src/build_support/dependencies.json +++ b/lib/src/build_support/dependencies.json @@ -65,6 +65,16 @@ "url": "https://github.com/ech-research/libechhttp-darwin-build/releases/download/v0.1.2/libechhttp-deps-v0.1.2-macos-x64.zip", "sha256": "f81024344f6542ed33d3c197087ab34f94525f38efc04213508c1eaec44c4a0f" }, + "ohos-arm64": { + "release": "v0.1.0", + "url": "https://github.com/tignioj/libechhttp_ohos_build/releases/download/v0.1.0/libechhttp-deps-v0.1.0-ohos-arm64.zip", + "sha256": "986aaea773733301b66a1f94a1986f71b1a31f54e89c4909fcc2fd4b0ea982fc" + }, + "ohos-x64": { + "release": "v0.1.0", + "url": "https://github.com/tignioj/libechhttp_ohos_build/releases/download/v0.1.0/libechhttp-deps-v0.1.0-ohos-x64.zip", + "sha256": "240eb7c8eb2660302f5e73a2fc28c7a43326874c10522a474034cabcd7953bdc" + }, "windows-arm64": { "release": "v0.1.1", "url": "https://github.com/ech-research/libechhttp-win32-build/releases/download/v0.1.1/libechhttp-deps-v0.1.1-windows-arm64.zip", diff --git a/test/ohos_build_test.dart b/test/ohos_build_test.dart index e9c80d2..0f2bc11 100644 --- a/test/ohos_build_test.dart +++ b/test/ohos_build_test.dart @@ -58,19 +58,16 @@ void main() { expect(config.abi, 'x86_64'); }); - test( - 'supports explicit source-build configuration with a native SDK path', - () { - final config = OhosBuildConfig.detect( - targetOS: OS.linux, - architecture: Architecture.arm64, - explicitlyOhos: true, - sdkPath: native.path, - environment: {}, - )!; - expect(config.nativeSdk, native.path); - }, - ); + test('supports an explicit OHOS configuration with a native SDK path', () { + final config = OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + explicitlyOhos: true, + sdkPath: native.path, + environment: {}, + )!; + expect(config.nativeSdk, native.path); + }); test('missing SDK fails with actionable configuration guidance', () { expect( @@ -89,16 +86,19 @@ void main() { ); }); - test('unsupported OHOS architecture fails before compiling dependencies', () { - expect( - () => OhosBuildConfig.detect( - targetOS: OS.fromString('ohos'), - architecture: Architecture.arm, - sdkPath: native.path, - ), - throwsUnsupportedError, - ); - }); + test( + 'unsupported OHOS architecture fails before downloading dependencies', + () { + expect( + () => OhosBuildConfig.detect( + targetOS: OS.fromString('ohos'), + architecture: Architecture.arm, + sdkPath: native.path, + ), + throwsUnsupportedError, + ); + }, + ); test('explicit OHOS override cannot mislabel a macOS asset', () { expect( diff --git a/test/prebuilt_test.dart b/test/prebuilt_test.dart index 64c468b..95862d8 100644 --- a/test/prebuilt_test.dart +++ b/test/prebuilt_test.dart @@ -21,14 +21,16 @@ List _sdk({ String zlibVersion = '1.3.2', String zlibDigest = _zlibDigest, }) { + final windows = target.startsWith('windows-'); + String library(String name) => windows ? '$name.lib' : 'lib$name.a'; final files = { 'cmake/EchHttpDeps.cmake': 'verified cmake', 'include/curl/curl.h': 'curl header', 'include/openssl/ssl.h': 'ssl header', - 'lib/curl.lib': 'curl static library', - 'lib/ssl.lib': 'ssl static library', - if (!omitCrypto) 'lib/crypto.lib': 'crypto static library', - if (!omitZlib) 'lib/zlib.lib': 'zlib static library', + 'lib/${library('curl')}': 'curl static library', + 'lib/${library('ssl')}': 'ssl static library', + if (!omitCrypto) 'lib/${library('crypto')}': 'crypto static library', + if (!omitZlib) 'lib/${library('zlib')}': 'zlib static library', 'include/zlib.h': 'zlib header', 'include/zconf.h': 'zlib configuration', 'licenses/ZLIB_LICENSE': 'zlib license', @@ -57,16 +59,17 @@ List _sdk({ return ZipEncoder().encode(archive); } -PrebuiltDependency _pin(List bytes) => PrebuiltDependency( - target: 'windows-x64', - release: 'v0.1.0', - url: Uri.https('example.test', '/dependency.zip'), - digest: sha256.convert(bytes).toString(), - curlVersion: '8.22.0', - boringRevision: _revision, - zlibVersion: '1.3.2', - zlibDigest: _zlibDigest, -); +PrebuiltDependency _pin(List bytes, {String target = 'windows-x64'}) => + PrebuiltDependency( + target: target, + release: 'v0.1.0', + url: Uri.https('example.test', '/dependency.zip'), + digest: sha256.convert(bytes).toString(), + curlVersion: '8.22.0', + boringRevision: _revision, + zlibVersion: '1.3.2', + zlibDigest: _zlibDigest, + ); void main() { late Directory cache; @@ -115,6 +118,42 @@ void main() { }, ); + for (final target in ['ohos-arm64', 'ohos-x64']) { + test('$target SDK is verified, cached offline and repaired', () async { + final bytes = _sdk(target: target); + final pin = _pin(bytes, target: target); + final sdk = await preparePrebuilt( + pin, + cache.path, + download: (_, file) => file.writeAsBytes(bytes), + ); + final library = File(p.join(sdk, 'lib', 'libcurl.a')); + expect(await library.readAsString(), 'curl static library'); + await library.writeAsString('tampered'); + expect( + await preparePrebuilt( + pin, + cache.path, + download: (_, _) => throw StateError('Cached SDK must work offline'), + ), + sdk, + ); + expect(await library.readAsString(), 'curl static library'); + }); + } + + test('OHOS SDK cannot be used for the other architecture', () async { + final bytes = _sdk(target: 'ohos-x64'); + await expectLater( + preparePrebuilt( + _pin(bytes, target: 'ohos-arm64'), + cache.path, + download: (_, file) => file.writeAsBytes(bytes), + ), + throwsStateError, + ); + }); + test( 'corrupt cached archive is replaced only by a matching download', () async { diff --git a/tool/build_ohos.py b/tool/build_ohos.py deleted file mode 100644 index 1cf9ad5..0000000 --- a/tool/build_ohos.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 -"""Cross-compile ech_http with pinned BoringSSL, curl and zlib for OpenHarmony. - -Requires Python 3.12+, Git, CMake, Ninja, and an OpenHarmony native SDK. -No Android binaries or host TLS libraries are used. Downloads are cached. -""" -import argparse -import hashlib -import json -import os -from pathlib import Path -import shutil -import subprocess -import sys -import tarfile -import urllib.request - -ROOT = Path(__file__).resolve().parents[1] -ARCHITECTURES = {'arm64': 'arm64-v8a', 'x64': 'x86_64'} - - -def run(args): - print('+', ' '.join(map(str, args)), flush=True) - subprocess.run(list(map(str, args)), check=True) - - -def configure(cmake, source, build, arguments, identity=None): - # Configure probes and object files must not survive a dependency or SDK - # change. Downloads live outside these generated build directories. - fingerprint = json.dumps({ - 'schema': 1, - 'source': str(source.resolve()), - 'arguments': list(map(str, arguments)), - 'identity': identity, - }, sort_keys=True) - stamp = build / '.ech-http-config.json' - if build.exists() and (not stamp.is_file() or stamp.read_text() != fingerprint): - shutil.rmtree(build) - run([cmake, '-S', source, '-B', build, *arguments]) - stamp.write_text(fingerprint) - - -def digest(path): - with path.open('rb') as stream: - return hashlib.file_digest(stream, 'sha256').hexdigest() - - -def native_sdk(path): - root = Path(path).expanduser().resolve() - for candidate in [root, root / 'native', root / 'openharmony/native', - root / 'default/openharmony/native']: - if (candidate / 'build/cmake/ohos.toolchain.cmake').is_file(): - return candidate - raise ValueError(f'OpenHarmony native SDK not found under {root}') - - -def download_source(pin, cache, name): - archive = cache / Path(pin['url']).name - if not archive.is_file() or digest(archive) != pin['sha256']: - partial = archive.with_suffix(archive.suffix + '.part') - try: - with urllib.request.urlopen(pin['url'], timeout=60) as source, partial.open('wb') as output: - shutil.copyfileobj(source, output) - if digest(partial) != pin['sha256']: - raise RuntimeError(f'{name} source SHA-256 mismatch') - partial.replace(archive) - finally: - partial.unlink(missing_ok=True) - directory = cache / f"{name}-{pin['version']}" - if not directory.is_dir(): - with tarfile.open(archive) as tar: - tar.extractall(cache, filter='data') - return directory - - -def sources(manifest, cache): - cache.mkdir(parents=True, exist_ok=True) - revision = manifest['boringssl']['revision'] - boring = cache / f'boringssl-{revision}' - if not (boring / '.git').is_dir(): - run(['git', 'init', boring]) - run(['git', '-C', boring, 'remote', 'add', 'origin', manifest['boringssl']['repository']]) - if not (boring / 'CMakeLists.txt').is_file(): - run(['git', '-C', boring, 'fetch', '--depth=1', 'origin', revision]) - run(['git', '-C', boring, 'checkout', '--detach', revision]) - actual = subprocess.check_output(['git', '-C', str(boring), 'rev-parse', 'HEAD'], text=True).strip() - if actual != revision: - raise RuntimeError('BoringSSL source revision mismatch') - return boring, download_source(manifest['curl'], cache, 'curl'), download_source(manifest['zlib'], cache, 'zlib') - - -def main(): - if sys.version_info < (3, 12): - raise SystemExit('OpenHarmony source builds require Python >=3.12') - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--native-sdk', default=os.environ.get('OHOS_NDK_HOME') or os.environ.get('HOS_SDK_HOME')) - parser.add_argument('--dart-sdk', type=Path) - parser.add_argument('--arch', choices=ARCHITECTURES, default='arm64') - parser.add_argument('--output', type=Path) - parser.add_argument('--source-cache', type=Path, default=ROOT / 'build/ohos/sources') - parser.add_argument('--deps-only', action='store_true') - options = parser.parse_args() - if not options.native_sdk: - parser.error('Set OHOS_NDK_HOME or pass --native-sdk pointing to an OpenHarmony SDK') - if not options.deps_only and (options.dart_sdk is None or not (options.dart_sdk / 'include/dart_native_api.h').is_file()): - parser.error('--dart-sdk must point to a Dart SDK containing include/dart_native_api.h') - sdk = native_sdk(options.native_sdk) - work = (options.output or ROOT / 'build/ohos' / options.arch).resolve() - work.mkdir(parents=True, exist_ok=True) - suffix = '.exe' if os.name == 'nt' else '' - cmake = shutil.which('cmake') or str(sdk / f'build-tools/cmake/bin/cmake{suffix}') - ninja = shutil.which('ninja') or str(sdk / f'build-tools/cmake/bin/ninja{suffix}') - if not Path(cmake).is_file() or not Path(ninja).is_file(): - parser.error('Install CMake >=3.22 and Ninja, or add the native SDK build-tools/cmake/bin to PATH') - common = ['-G', 'Ninja', f'-DCMAKE_MAKE_PROGRAM={ninja}', '-DCMAKE_BUILD_TYPE=Release', - f'-DCMAKE_TOOLCHAIN_FILE={sdk}/build/cmake/ohos.toolchain.cmake', - f'-DOHOS_ARCH={ARCHITECTURES[options.arch]}', '-DOHOS_PLATFORM=OHOS', - '-DOHOS_STL=c++_static', '-DCMAKE_POSITION_INDEPENDENT_CODE=ON', - '-DCMAKE_C_VISIBILITY_PRESET=hidden', '-DCMAKE_CXX_VISIBILITY_PRESET=hidden', - # The SDK adds --gcc-toolchain even for libc++; BoringSSL's - # -Werror must not reject this SDK-supplied, unused argument. - '-DCMAKE_C_FLAGS=-Wno-unused-command-line-argument', - '-DCMAKE_CXX_FLAGS=-Wno-unused-command-line-argument'] - manifest = json.loads((ROOT / 'lib/src/build_support/dependencies.json').read_text()) - sdk_metadata = json.loads((sdk / 'oh-uni-package.json').read_text()) - identity = { - 'dependencies': {name: manifest[name] for name in ('boringssl', 'curl', 'zlib')}, - 'native_sdk': sdk_metadata, - 'toolchain_sha256': digest(sdk / 'build/cmake/ohos.toolchain.cmake'), - } - boring, curl, zlib = sources(manifest, options.source_cache.resolve()) - parallel = str(min(os.cpu_count() or 2, 8)) - boring_build = work / 'boringssl' - configure(cmake, boring, boring_build, [*common, '-DBUILD_SHARED_LIBS=OFF', - '-DBUILD_TESTING=OFF', '-DOPENSSL_NO_ASM=ON', '-DOPENSSL_SMALL=ON'], identity) - run([cmake, '--build', boring_build, '--target', 'ssl', 'crypto', '--parallel', parallel]) - zlib_build = work / 'zlib' - configure(cmake, zlib, zlib_build, [*common, '-DZLIB_BUILD_SHARED=OFF', - '-DZLIB_BUILD_STATIC=ON', '-DZLIB_BUILD_TESTING=OFF'], identity) - run([cmake, '--build', zlib_build, '--target', 'zlibstatic', '--parallel', parallel]) - includes = work / 'zlib-include' - includes.mkdir(exist_ok=True) - shutil.copyfile(zlib / 'zlib.h', includes / 'zlib.h') - shutil.copyfile(zlib_build / 'zconf.h', includes / 'zconf.h') - curl_build = work / 'curl' - configure(cmake, ROOT / 'tool/ohos', curl_build, [*common, - f'-DEH_CURL_SOURCE={curl}', f'-DEH_BORINGSSL_SOURCE={boring}', - f'-DEH_BORINGSSL_BUILD={boring_build}', f'-DZLIB_LIBRARY={zlib_build}/libz.a', - f'-DZLIB_INCLUDE_DIR={includes}'], identity) - run([cmake, '--build', curl_build, '--parallel', parallel]) - if 'HAVE_SSL_SET1_ECH_CONFIG_LIST:INTERNAL=1' not in (curl_build / 'CMakeCache.txt').read_text(): - raise RuntimeError('libcurl was built without BoringSSL ECH support') - if '#define HAVE_LIBZ 1' not in (curl_build / 'curl/lib/curl_config.h').read_text(): - raise RuntimeError('libcurl was built without zlib support') - stage = work / 'sdk' - (stage / 'lib').mkdir(parents=True, exist_ok=True) - shutil.copytree(boring / 'include/openssl', stage / 'include/openssl', dirs_exist_ok=True) - shutil.copytree(curl / 'include/curl', stage / 'include/curl', dirs_exist_ok=True) - shutil.copytree(includes, stage / 'include', dirs_exist_ok=True) - for source, name in [(curl_build / 'curl/lib/libcurl.a', 'curl'), - (boring_build / 'libssl.a', 'ssl'), (boring_build / 'libcrypto.a', 'crypto'), - (zlib_build / 'libz.a', 'zlib')]: - shutil.copyfile(source, stage / f'lib/lib{name}.a') - (stage / 'cmake').mkdir(exist_ok=True) - shutil.copyfile(ROOT / 'tool/ohos/EchHttpDeps.cmake', stage / 'cmake/EchHttpDeps.cmake') - (stage / 'metadata.json').write_text(json.dumps({ - 'schema': 1, 'target': f'ohos-{options.arch}', - 'curl': manifest['curl'], 'boringssl': manifest['boringssl'], 'zlib': manifest['zlib'], - 'native_sdk': sdk_metadata, - }, indent=2) + '\n') - if options.deps_only: - return - bridge = work / 'bridge' - configure(cmake, ROOT / 'src', bridge, [*common, - f'-DEH_DEPS_ROOT={stage}', f'-DEH_DART_INCLUDE={options.dart_sdk.resolve()}/include', - '-DEH_BUILD_SMOKE=ON'], identity) - run([cmake, '--build', bridge, '--parallel', parallel]) - print(f'OpenHarmony library: {bridge}/out/libech_http.so', flush=True) - - -if __name__ == '__main__': - main() diff --git a/tool/ohos/CMakeLists.txt b/tool/ohos/CMakeLists.txt deleted file mode 100644 index 116ad18..0000000 --- a/tool/ohos/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -# The dependency options match ech-research/libechhttp-android-build (MIT, -# copyright 2026 Predidit). Use the same pinned headers and static libraries. -cmake_minimum_required(VERSION 3.22) -project(ech_http_ohos_dependencies LANGUAGES C CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_POSITION_INDEPENDENT_CODE ON) -set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) -set(BUILD_TESTING OFF CACHE BOOL "" FORCE) -set(BUILD_LIBCURL_DOCS OFF CACHE BOOL "" FORCE) -set(BUILD_MISC_DOCS OFF CACHE BOOL "" FORCE) -set(ENABLE_CURL_MANUAL OFF CACHE BOOL "" FORCE) -set(CURL_USE_OPENSSL ON CACHE BOOL "" FORCE) -set(CURL_USE_SCHANNEL OFF CACHE BOOL "" FORCE) -set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE) -set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE) -set(CURL_USE_LIBSSH OFF CACHE BOOL "" FORCE) -set(CURL_USE_GSSAPI OFF CACHE BOOL "" FORCE) -set(CURL_ZLIB ON CACHE BOOL "" FORCE) -set(CURL_BROTLI OFF CACHE BOOL "" FORCE) -set(CURL_ZSTD OFF CACHE BOOL "" FORCE) -set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE) -set(USE_LIBIDN2 OFF CACHE BOOL "" FORCE) -set(USE_ECH ON CACHE BOOL "" FORCE) -set(CURL_CA_BUNDLE "none" CACHE STRING "" FORCE) -set(CURL_CA_PATH "none" CACHE STRING "" FORCE) -set(CURL_CA_FALLBACK OFF CACHE BOOL "" FORCE) -set(HTTP_ONLY ON CACHE BOOL "" FORCE) -set(OPENSSL_INCLUDE_DIR "${EH_BORINGSSL_SOURCE}/include" CACHE PATH "" FORCE) -set(OPENSSL_SSL_LIBRARY "${EH_BORINGSSL_BUILD}/libssl.a" CACHE FILEPATH "" FORCE) -set(OPENSSL_CRYPTO_LIBRARY "${EH_BORINGSSL_BUILD}/libcrypto.a" CACHE FILEPATH "" FORCE) -find_package(OpenSSL REQUIRED) -# curl's C configure probes also need BoringSSL's C++ runtime. -set_property(TARGET OpenSSL::SSL APPEND PROPERTY INTERFACE_LINK_LIBRARIES - ${CMAKE_CXX_IMPLICIT_LINK_LIBRARIES}) -find_package(ZLIB REQUIRED) -add_subdirectory("${EH_CURL_SOURCE}" curl) diff --git a/tool/ohos/EchHttpDeps.cmake b/tool/ohos/EchHttpDeps.cmake deleted file mode 100644 index 0854ab2..0000000 --- a/tool/ohos/EchHttpDeps.cmake +++ /dev/null @@ -1,15 +0,0 @@ -if(TARGET EchHttp::curl) - return() -endif() -get_filename_component(_eh_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) -foreach(_eh_lib curl ssl crypto zlib) - add_library(EchHttp::${_eh_lib} STATIC IMPORTED) - set_target_properties(EchHttp::${_eh_lib} PROPERTIES - IMPORTED_LOCATION "${_eh_root}/lib/lib${_eh_lib}.a" - INTERFACE_INCLUDE_DIRECTORIES "${_eh_root}/include") -endforeach() -find_package(Threads REQUIRED) -set_property(TARGET EchHttp::curl PROPERTY INTERFACE_COMPILE_DEFINITIONS CURL_STATICLIB) -set_property(TARGET EchHttp::curl PROPERTY INTERFACE_LINK_LIBRARIES EchHttp::ssl EchHttp::zlib) -set_property(TARGET EchHttp::ssl PROPERTY INTERFACE_LINK_LIBRARIES EchHttp::crypto) -set_property(TARGET EchHttp::crypto PROPERTY INTERFACE_LINK_LIBRARIES Threads::Threads ${CMAKE_DL_LIBS} m) diff --git a/tool/test_build_ohos.py b/tool/test_build_ohos.py deleted file mode 100644 index ac2f034..0000000 --- a/tool/test_build_ohos.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Offline regression tests for the OHOS CMake cache lifecycle.""" -from pathlib import Path -import shutil -import subprocess -from tempfile import TemporaryDirectory -import unittest - -from build_ohos import configure - - -@unittest.skipUnless(shutil.which('cmake'), 'CMake is required') -class ConfigureTest(unittest.TestCase): - def setUp(self): - self.temporary = TemporaryDirectory(prefix='ech-ohos-cache-') - self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) - self.source = self.root / 'source-v1' - self.source.mkdir() - (self.source / 'CMakeLists.txt').write_text( - 'cmake_minimum_required(VERSION 3.22)\n' - 'project(cache_test NONE)\n' - 'set(CACHED_RESULT "${PIN}" CACHE STRING "configure probe")\n' - ) - self.build = self.root / 'build' - self.download = self.root / 'sources' / 'archive.tar' - self.download.parent.mkdir() - self.download.write_text('cached download') - - def check_configure(self, source=None, pin='one', identity=None): - configure('cmake', source or self.source, self.build, - [f'-DPIN={pin}'], identity) - self.assertEqual(self.download.read_text(), 'cached download') - - def test_unchanged_configuration_preserves_incremental_outputs(self): - self.check_configure() - artifact = self.build / 'incremental-output' - artifact.write_text('keep') - self.check_configure() - self.assertEqual(artifact.read_text(), 'keep') - - def test_dependency_source_can_move_to_a_new_revision(self): - self.check_configure() - source = self.root / 'source-v2' - shutil.copytree(self.source, source) - self.check_configure(source=source) - self.assertIn(f'CMAKE_HOME_DIRECTORY:INTERNAL={source}', - (self.build / 'CMakeCache.txt').read_text()) - - def test_changed_arguments_repeat_cached_configure_probes(self): - self.check_configure() - self.check_configure(pin='two') - self.assertIn('CACHED_RESULT:STRING=two', - (self.build / 'CMakeCache.txt').read_text()) - - def test_dependency_and_sdk_updates_discard_generated_outputs(self): - for identity in ({'dependency': 'v1', 'sdk': 'v1'}, - {'dependency': 'v2', 'sdk': 'v1'}, - {'dependency': 'v2', 'sdk': 'v2'}): - with self.subTest(identity=identity): - self.check_configure(identity=identity) - artifact = self.build / 'obsolete-output' - self.assertFalse(artifact.exists()) - artifact.write_text('remove on change') - - def test_migrates_a_build_without_a_fingerprint(self): - subprocess.run(['cmake', '-S', str(self.source), '-B', str(self.build), - '-DPIN=legacy'], check=True, capture_output=True) - self.check_configure() - self.assertIn('CACHED_RESULT:STRING=one', - (self.build / 'CMakeCache.txt').read_text()) - - -if __name__ == '__main__': - unittest.main() diff --git a/tool/update_prebuilt.py b/tool/update_prebuilt.py index fbc8f1c..b973e8a 100644 --- a/tool/update_prebuilt.py +++ b/tool/update_prebuilt.py @@ -11,12 +11,16 @@ 'darwin': ['macos-x64', 'macos-arm64', 'ios-arm64-device', 'ios-arm64-simulator', 'ios-x64-simulator'], 'android': ['android-arm', 'android-arm64', 'android-x64', 'android-ia32'], 'linux': ['linux-x64', 'linux-arm64'], + 'ohos': ['ohos-arm64', 'ohos-x64'], } +# Update this mapping after an authorized transfer to ech-research. +REPOSITORIES = {'ohos': 'tignioj/libechhttp_ohos_build'} + def main(): if not sys.argv[1:]: - raise SystemExit('Usage: python tool/update_prebuilt.py win32=v0.1.0 darwin=v0.1.1 android=v0.1.0 linux=v0.1.0') + raise SystemExit('Usage: python tool/update_prebuilt.py win32=v0.1.0 darwin=v0.1.1 android=v0.1.0 linux=v0.1.0 ohos=v0.1.0') path = Path(__file__).resolve().parents[1] / 'lib/src/build_support/dependencies.json' manifest = json.loads(path.read_text()) targets = manifest.setdefault('targets', {}) @@ -24,7 +28,7 @@ def main(): family, tag = item.split('=', 1) if family not in TARGETS or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+', tag): raise SystemExit(f'Invalid family or version: {item}') - repo = f'ech-research/libechhttp-{family}-build' + repo = REPOSITORIES.get(family, f'ech-research/libechhttp-{family}-build') release = json.loads(subprocess.check_output([ 'gh', 'api', f'repos/{repo}/releases/tags/{tag}', ], text=True, encoding='utf-8')) From bb27c8092e2610b474a1488b9bd808e0d3b738fd Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 14:07:26 +0800 Subject: [PATCH 6/8] docs(ohos): record precompiled SDK device verification --- doc/ohos.md | 47 +++++++++++++++++++++++++++++++++++---- doc/ohos.zh-CN.md | 42 ++++++++++++++++++++++++++++++---- doc/verification.md | 2 +- doc/verification.zh-CN.md | 2 +- 4 files changed, 83 insertions(+), 10 deletions(-) diff --git a/doc/ohos.md b/doc/ohos.md index 14cb74f..acb3e05 100644 --- a/doc/ohos.md +++ b/doc/ohos.md @@ -122,6 +122,45 @@ builds do not use that AOT compiler. ## Verification scope +### Precompiled SDK device rerun (2026-10-17) + +A signed Debug HAP using the published precompiled SDK was reinstalled and +launched on MatePad mini (MLR-AL10, API 26). The tested `ech_http` implementation +was commit `0a94849d7e1efba33abafd3a2529cf8dca4a2268`, with the `v0.1.0` arm64 SDK +built from `6ebb0d4b4617115f4a2cc7f1b324d861e222c9a2`. The SDK archive SHA-256 was +`986aaea773733301b66a1f94a1986f71b1a31f54e89c4909fcc2fd4b0ea982fc`. + +The app was rebuilt with Flutter-OH 3.41.9+ohos-1.0.2, Dart 3.11.5, and HarmonyOS +SDK 26.0.0.105 after clearing its `.dart_tool` and Flutter build outputs. Local +checks verified the downloaded archive digest, precompiled static-library link +paths, HAP arm64 library, and Native Assets mapping. The packaged library exactly +matched the newly built bridge after `llvm-strip`; its sole ELF dynamic dependency +was `libc.so`. The HAP was signed with the existing local debug identity, passed +signature verification, and was successfully installed over the test app. + +At 14:05:48 (Asia/Shanghai), the device returned the unique marker for this run. +Native Assets loading and FFI calls succeeded, including the backend version. +After discovering ECH configuration over DoH, the request to +`https://crypto.cloudflare.com/cdn-cgi/trace` returned: + +```text +ECH_HTTP_PREBUILT_V0_1_0_0a94849: libcurl/8.22.0 BoringSSL zlib/1.3.2 +HTTP 200 +ECH accepted: true +tls=TLSv1.3 +sni=encrypted +``` + +The raw local capture is `build/prebuilt-ohos-runtime-smoke.log`; provenance and +assertion results are in `build/prebuilt-device-result.json`. These generated +files are not published with the package. This rerun covers arm64 signed Debug +HAP loading and live HTTPS/ECH. It does not cover signed Release HAP execution, +compatibility below API 26, x64 devices, or individual OHOS device regression +checks for proxies, gzip, cancellation, and isolate shutdown. Desktop coverage +for these features does not establish OHOS device verification. + +### Initial source-build record + The initial source-build verification on 2026-10-17 used a macOS arm64 host, Dart 3.13.5, and OpenHarmony native SDK 6.0.0.47 (API 20). The Flutter HAP used Flutter-OH 3.41.9+ohos-1.0.2, its bundled Dart 3.11.5, and HarmonyOS SDK @@ -157,8 +196,8 @@ file, not part of the package). Device verification covers Native Assets integration and public HTTPS/ECH. gzip, proxies, cancellation, and isolate shutdown have desktop regression coverage but have not each been repeated on an OHOS device. x64 has build coverage only. Release HAP and remote CI -verification were not part of the initial local device run. Those historical -device results do not mean the precompiled SDK version has been rerun on a device. +verification were not part of the initial local device run. The separate +precompiled SDK device rerun is recorded above. The dependency repository's [initial SDK CI](https://github.com/tignioj/libechhttp_ohos_build/actions/runs/39006660450) verified arm64/x64 dependency and packaged consumer links, ELF architecture, @@ -195,5 +234,5 @@ SDKs, builds only the bridge, and checks arm64/x64 hook outputs plus unsigned Release HAP contents and Native Assets mapping with pinned Flutter-OH. Neither pipeline replaces signed device execution. x64 still has no device coverage. Proxy, gzip, cancellation, and isolate shutdown have desktop tests -but have not each been repeated on an OHOS device. Rerunning the precompiled -SDK version on a device is outside these automated build checks. +but have not each been repeated on an OHOS device. The precompiled SDK device +rerun above is manual verification, separate from automated CI. diff --git a/doc/ohos.zh-CN.md b/doc/ohos.zh-CN.md index 3d9a223..583dc51 100644 --- a/doc/ohos.zh-CN.md +++ b/doc/ohos.zh-CN.md @@ -109,6 +109,41 @@ Debug 测试包不依赖这个 AOT 编译器。 ## 验证范围 +### 预编译 SDK 真机复测(2026-10-17) + +在 MatePad mini(MLR-AL10,API 26)上重新安装并启动了使用预编译 SDK 的签名 +Debug HAP。本次测试的 `ech_http` 实现提交为 +`0a94849d7e1efba33abafd3a2529cf8dca4a2268`,依赖使用已发布的 `v0.1.0` +arm64 SDK(构建提交 `6ebb0d4b4617115f4a2cc7f1b324d861e222c9a2`)。 +SDK 归档 SHA-256 为 +`986aaea773733301b66a1f94a1986f71b1a31f54e89c4909fcc2fd4b0ea982fc`。 + +使用 Flutter-OH 3.41.9+ohos-1.0.2、Dart 3.11.5 和 HarmonyOS SDK 26.0.0.105, +清理应用 `.dart_tool` 与 Flutter 构建输出后重新构建。本地确认下载归档摘要、 +预编译静态库链接路径、HAP 中的 arm64 库和 Native Assets 映射;包内库与本次 +胶水代码构建产物经 `llvm-strip` 后完全一致,ELF 动态依赖仅为 `libc.so`。 +HAP 使用已有的本机调试身份签名并通过签名验证,随后成功覆盖安装到设备。 + +设备于 14:05:48(Asia/Shanghai)返回本次专用日志标记。Native Assets 库加载及 +FFI 调用成功,后端版本正常返回;通过 DoH 获取 ECH 配置后,请求 +`https://crypto.cloudflare.com/cdn-cgi/trace` 返回: + +```text +ECH_HTTP_PREBUILT_V0_1_0_0a94849: libcurl/8.22.0 BoringSSL zlib/1.3.2 +HTTP 200 +ECH accepted: true +tls=TLSv1.3 +sni=encrypted +``` + +原始设备日志保存在本地 `build/prebuilt-ohos-runtime-smoke.log`,产物来源与断言结果 +保存在 `build/prebuilt-device-result.json`;两者均为生成文件,不随包发布。 +本次为 arm64 签名 Debug HAP 的加载和公网 HTTPS/ECH 验证,不包含 Release HAP +真机运行、低于 API 26 的兼容性、x64 设备,以及代理、gzip、取消请求和 isolate +退出的逐项鸿蒙真机回归。这些功能的桌面测试覆盖不等同于鸿蒙真机验证。 + +### 初始源码构建记录 + 2026-10-17 初始源码构建版本的本地检查使用 macOS arm64 构建机、Dart 3.13.5、OpenHarmony native SDK 6.0.0.47(API 20)。Flutter HAP 使用 Flutter-OH 3.41.9+ohos-1.0.2、其内置 Dart 3.11.5 和 HarmonyOS SDK 26.0.0.105。 @@ -143,8 +178,7 @@ sni=encrypted Native Assets 集成和公网 HTTPS/ECH;gzip、代理、取消请求、isolate 退出 等行为已覆盖桌面回归测试,尚未在鸿蒙真机上逐项复测。 x64 仅完成交叉编译和构建钩子检查。初次本地真机验证未包含 Release HAP -与远程 CI。改为预编译 SDK 后,应以本次 SDK CI 与消费方 CI 为准; -此前真机记录不等同于已在设备上复测预编译 SDK 版本。 +与远程 CI。预编译 SDK 版本的独立真机复测结果见上文。 预编译依赖仓库的[首版 CI](https://github.com/tignioj/libechhttp_ohos_build/actions/runs/39006660450) 已验证 arm64、x64 的依赖与独立消费方链接、ELF 架构、动态依赖、归档摘要、 @@ -175,5 +209,5 @@ dart test test/ohos_build_test.dart test/prebuilt_test.dart 本包 CI 从已发布 Release 下载 SDK,仅编译胶水代码,并检查 arm64/x64 构建钩子输出及固定 Flutter-OH 工具链的未签名 Release HAP 打包和映射。 两类 CI 都不替代签名 HAP 的真机运行。x64 尚未进行真机验证;代理、gzip、 -取消请求和 isolate 退出尚未在鸿蒙真机逐项复测。预编译 SDK 版本的设备复测 -不包含在本次自动化构建验证中。 +取消请求和 isolate 退出尚未在鸿蒙真机逐项复测。上述预编译 SDK 真机复测 +为手动设备验证,不属于自动化 CI。 diff --git a/doc/verification.md b/doc/verification.md index 47d6cee..62cbd23 100644 --- a/doc/verification.md +++ b/doc/verification.md @@ -42,7 +42,7 @@ We distinguish between three tiers of verification: | **Android** | `arm64-v8a` | **Tier 1** | Physical device run (API 35), live ECH handshake, authenticated retry, connection reuse | Non-standard custom Android ROMs | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK packaging for both ABIs | Runtime execution on physical 32-bit ARM / x86_64 devices | | **Android** | `x86` | **Tier 2** | NDK bridge build and link check in CI | 32-bit x86 Android runtime execution | -| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | arm64/x64 precompiled SDKs and bridge builds; an earlier source-build signed Debug HAP returned HTTP 200 with accepted ECH on MatePad mini | See the [OHOS guide](ohos.md) for runtime scope; precompiled SDK device rerun pending; full suite not repeated on OHOS, no x64 device attached | +| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | arm64/x64 precompiled SDK and bridge builds; on 2026-10-17 a signed Debug HAP using SDK v0.1.0 loaded the native library on MatePad mini (API 26), with HTTP 200, accepted ECH, `sni=encrypted`, and TLS 1.3 | See [OHOS scope](ohos.md); full suite, signed Release HAP, and lower API compatibility not verified on OHOS, no x64 device attached | | **Web** | All | **Tier 3** | Not supported by this native C++ FFI architecture | N/A | --- diff --git a/doc/verification.zh-CN.md b/doc/verification.zh-CN.md index 95de8ef..39ec85f 100644 --- a/doc/verification.zh-CN.md +++ b/doc/verification.zh-CN.md @@ -37,7 +37,7 @@ Flutter Android APK、iOS 未签名 Release 应用、iOS 模拟器 Debug 应用 | **Android** | `arm64-v8a` | **Tier 1** | 真实设备运行(API 35),公网 ECH 握手、认证重试、连接复用均通过 | 各类深度定制的非标准第三方 Android ROM | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK 多 ABI 打包测试通过 | 32 位 ARM / x86_64 物理机运行时执行 | | **Android** | `x86` | **Tier 2** | CI 中 NDK 桥接构建与链接核验通过 | 32 位 x86 Android 实体机运行时执行 | -| **OpenHarmony / HarmonyOS** | arm64、x64 | **Tier 2** | arm64/x64 预编译 SDK 与胶水代码构建;此前源码构建版本的签名 Debug HAP 在 MatePad mini 返回 HTTP 200、ECH 被接受 | 真机验证范围见[鸿蒙说明](ohos.zh-CN.md);预编译 SDK 版本尚未复测签名真机;完整功能套件未在鸿蒙复测,未连接 x64 设备 | +| **OpenHarmony / HarmonyOS** | arm64、x64 | **Tier 2** | arm64/x64 预编译 SDK 与胶水代码构建;2026-10-17 使用 v0.1.0 SDK 的签名 Debug HAP 在 MatePad mini(API 26)成功加载原生库,返回 HTTP 200、ECH 被接受、`sni=encrypted`、TLS 1.3 | 真机验证范围见[鸿蒙说明](ohos.zh-CN.md);完整功能套件、签名 Release HAP 和低 API 兼容性未在鸿蒙复测,未连接 x64 设备 | | **Web** | 全部架构 | **Tier 3** | C++ FFI 原生引擎架构不支持 Web | 不适用 | --- From c44be858320b34a61d9c4eedc47b6be32c1d0126 Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 16:57:18 +0800 Subject: [PATCH 7/8] docs: integrate OHOS notes into existing platform documentation --- CHANGELOG.md | 15 +-- README.md | 19 ++- README.zh-CN.md | 17 ++- doc/ohos.md | 238 -------------------------------------- doc/ohos.zh-CN.md | 213 ---------------------------------- doc/releasing.md | 8 +- doc/releasing.zh-CN.md | 8 +- doc/verification.md | 2 +- doc/verification.zh-CN.md | 2 +- 9 files changed, 29 insertions(+), 493 deletions(-) delete mode 100644 doc/ohos.md delete mode 100644 doc/ohos.zh-CN.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e9138d0..7c58733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,23 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- OpenHarmony / HarmonyOS arm64 and x64 builds through Flutter-OH Native Assets, - using versioned BoringSSL, curl, and zlib dependency SDKs pinned by SHA-256. - Consumers compile only the C++ bridge; dependency builds live in the - independent `libechhttp_ohos_build` repository. -- OHOS SDK detection, cross-compilation and Flutter HAP smoke checks in CI, - and English and Chinese setup and verification guides. +- OpenHarmony / HarmonyOS arm64 and x64 support through Flutter-OH Native Assets + and precompiled dependency SDKs. ### Changed -- Allow compatible `code_assets` and `hooks` versions for Flutter-OH SDKs that - pin `meta` 1.17.0. +- Allow compatible `code_assets` and `hooks` versions for Flutter-OH's `meta` pin. ### Fixed -- Use the shared, content-addressed prebuilt cache for OHOS, including archive - and per-file integrity checks, offline reuse, and repair of modified files. - Reset bridge CMake metadata when the package moves between checkouts or caches. -- Use the original Dart SDK for CI package tools after adding Flutter-OH to PATH, - avoiding reuse of an incompatible hook kernel across Dart SDK versions. --- diff --git a/README.md b/README.md index 69b0bea..c176a71 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # ech_http -ech_http supports [OpenHarmony / HarmonyOS builds](doc/ohos.md) -using Flutter-OH Native Assets and verified precompiled dependency SDKs. -See that guide for toolchain requirements and verification limits. - [![Dart SDK](https://img.shields.io/badge/Dart-3.10+-0175C2.svg?logo=dart)](https://dart.dev) [![Flutter](https://img.shields.io/badge/Flutter-3.38+-02569B.svg?logo=flutter)](https://flutter.dev) [![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20iOS%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows%20%7C%20OHOS-blue.svg)](#supported-platforms-and-toolchain) @@ -63,7 +59,7 @@ prompt cleanup. - **Authenticated ECH Retries**: Supports up to 2 automated ECH retries when the server responds with a valid TLS `retry_configs` rejection. Public names are strictly validated before retrying. - **Separation of Destination Routing and SNI**: IP address overrides (`addresses`) redirect only the underlying TCP socket or proxy CONNECT tunnel. The TLS inner SNI, certificate identity verification, and HTTP `Host` header always correspond to the original requested hostname. - **Strict TLS Verification**: Every connection strictly validates the server's certificate chain and hostname against the bundled Mozilla CA root certificates. There is **no insecure mode** or certificate bypass switch. Custom PKI roots can be explicitly loaded via `trustedRootsPem`. -- **Automatic Native Builds**: Build hooks (`hook/build.dart`) download verified precompiled dependency SDKs for every supported native platform, including OHOS, and compile only the lightweight C++ bridge locally. BoringSSL, curl, and zlib are built and released by independent dependency repositories. +- **High-Performance Build Hooks**: Build hooks (`hook/build.dart`) automatically download verified precompiled dependency SDKs for the exact target platform, compiling only the lightweight C++ bridge locally (~15 s cold build). No need to compile BoringSSL or libcurl from scratch. - **Zero Runtime Daemons**: Communicates directly through Dart FFI within the host process memory space, avoiding child processes, socket proxy bottlenecks, and OS port conflicts. --- @@ -77,10 +73,13 @@ prompt cleanup. | **macOS** | `x64`, `arm64` | macOS host, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (device), `arm64`/`x64` (simulator) | macOS host, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (or Flutter bundled NDK), CMake, Ninja | Android API level 21+ (5.0 Lollipop) | -| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, CMake, Ninja; Flutter-OH Native Assets for HAPs | SDK API 26; see [OHOS validation](doc/ohos.md) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK, CMake, Ninja; Flutter-OH with Native Assets support | API 26 (verified) | + +For OHOS HAP builds, the hook locates the native SDK from the compiler path. +Set `OHOS_NDK_HOME` to the native SDK directory if needed. > [!NOTE] -> Web (Browsers) is not supported. All native platforms, including OHOS, use versioned dependency SDKs pinned by SHA-256. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. +> Web (Browsers) is not currently supported. Android binaries link against static libc++ and use 16 KiB ELF segment alignment for Android 15 compatibility. --- @@ -194,7 +193,7 @@ final client = EchClient( ## Offline Builds & Binary Caching -On all supported native targets, Dart build hooks download precompiled dependency SDKs on the first build and cache them under `.dart_tool`. For CI/CD environments or reproducible offline builds, configure a shared binary cache directory in your application's `pubspec.yaml`: +By default, Dart build hooks download precompiled dependency SDKs on the first build and cache them under `.dart_tool`. For CI/CD environments or reproducible offline builds, configure a shared binary cache directory in your application's `pubspec.yaml`: ```yaml hooks: @@ -207,10 +206,6 @@ hooks: - The build hook verifies the SHA-256 checksum of downloaded archives against `lib/src/build_support/dependencies.json`. - Cached archives are validated on each build; any damaged or modified extracted files are automatically repaired from the cached archive. -OHOS uses the same `binary_cache` mechanism. See the -[OHOS guide](doc/ohos.md#caching-and-dart-sdk-changes) for SDK baselines, -cache behavior, and switching between Dart SDKs. - --- ## Operational Limits & Design Constraints diff --git a/README.zh-CN.md b/README.zh-CN.md index a434924..e1f9afb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,9 +8,6 @@ [English](README.md) -`ech_http` 支持 [OpenHarmony / HarmonyOS 构建](doc/ohos.zh-CN.md), -通过 Flutter-OH Native Assets 下载经过校验的预编译 SDK,本地仅编译胶水代码,详见接入与验证说明。 - `ech_http` 是专为 Dart 和 Flutter 打造的企业级 HTTP 客户端,深度集成了 **TLS 密文客户端问候(Encrypted Client Hello, ECH)**、HTTP CONNECT 代理、自定义 PKI 信任根及流式数据传输功能。完全兼容官方 `package:http.Client` 接口规范。 底层核心由基于 **libcurl** 和 **BoringSSL** 构建的 C++17 原生引擎驱动,通过 Dart 现代构建钩子(Build Hooks / `code_assets`)实现跨平台自动打包。**在运行时,无需启动任何外部后台进程、无需部署本地监听代理、无需注册 Flutter 平台插件通道,也无需应用层手动管理动态链接库加载。** @@ -59,7 +56,7 @@ flowchart TD - **原生支持认证重试(Authenticated Retry)**:当服务端返回合法的 TLS `retry_configs`(配置拒绝重试)时,客户端会自动进行最多 2 次认证重试。重试前会严格校验外层公用名(Public Name)以防篡改。 - **物理路由与证书身份严格解耦**:自定义目标 IP 地址列表(`addresses`)仅重定向底层的 TCP Socket 物理连接或代理 CONNECT 隧道;TLS 握手中的内层 SNI、服务端证书主体身份核验以及 HTTP `Host` 请求头始终保持原始目标域名不变。 - **严格证书校验,无旁路后门**:所有连接默认基于内置的 Mozilla CA 根证书库严格验证证书链与主机名。**库中未设置任何跳过证书验证的不安全开关**。如需使用企业私有 PKI,可通过 `trustedRootsPem` 显式替换信任根。 -- **自动原生构建**:借助 Dart `hook/build.dart`,各原生平台均按需拉取经过校验的预编译 SDK,本地仅编译轻量级 C++ 桥接代码。BoringSSL、curl 和 zlib 由独立的依赖仓库提前编译、发布。 +- **极速构建钩子机制**:借助 Dart `hook/build.dart`,在首次构建时按需拉取对应平台的预编译 SDK,本地仅需编译轻量级的 C++ 桥接代码(冷构建仅约 15 秒),无需在本地完整编译庞大的 BoringSSL 与 libcurl。 - **零外部守护进程**:直接在应用主进程内存中通过 Dart FFI 调用原生接口,避免了子进程开销、本地端口占用冲突及系统级代理注入风险。 --- @@ -73,10 +70,13 @@ flowchart TD | **macOS** | `x64`, `arm64` | macOS 系统, Xcode Command Line Tools, CMake, Ninja | macOS 10.15+ (x64) / 11.0+ (arm64) | | **iOS** | `arm64` (真机), `arm64`/`x64` (模拟器) | macOS 系统, Xcode, CMake, Ninja | iOS 13.0+ | | **Android** | `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | Android NDK r28.2+ (或 Flutter 自带 NDK), CMake, Ninja | Android API Level 21+ (5.0 Lollipop) | -| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK、CMake、Ninja;HAP 需要 Flutter-OH Native Assets | SDK API 26;见[鸿蒙验证说明](doc/ohos.zh-CN.md) | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | OHOS native SDK、CMake、Ninja;支持 Native Assets 的 Flutter-OH | API 26(已验证) | + +构建 OHOS HAP 时,钩子从编译器路径定位 native SDK;需要时可通过 +`OHOS_NDK_HOME` 指定 native SDK 目录。 > [!NOTE] -> 当前不支持浏览器 Web 环境。包括鸿蒙在内的原生平台均使用固定版本、SHA-256 校验的预编译依赖 SDK。Android 原生库使用静态 libc++ 链接,并满足 Android 15 强制要求的 16 KiB ELF 页面对齐。 +> 当前暂不支持浏览器 Web 环境。Android 原生库使用静态 libc++ 链接,并满足 Android 15 强制要求的 16 KiB ELF 页面对齐。 --- @@ -190,7 +190,7 @@ final client = EchClient( ## 离线构建与二进制缓存配置 -各原生目标的构建钩子会在首次构建时自动下载依赖 SDK 并缓存在 `.dart_tool` 目录下。在 CI/CD 流水线或离线隔离环境中,可以在消费方项目的 `pubspec.yaml` 中指定持久化的共享缓存目录: +默认情况下,构建钩子会在首次构建时自动下载所需的依赖 SDK 并缓存在 `.dart_tool` 目录下。在 CI/CD 流水线或离线隔离环境中,可以在消费方项目的 `pubspec.yaml` 中指定持久化的共享缓存目录: ```yaml hooks: @@ -203,9 +203,6 @@ hooks: - 构建钩子会依据 `lib/src/build_support/dependencies.json` 中的 SHA-256 散列严格核验下载档案的完整性。 - 每次构建都会校验解压文件;若本地解压文件损坏或被修改,钩子会自动基于缓存的压缩包进行自愈修复。 -OHOS 同样使用 `binary_cache`;SDK 版本、缓存和 Dart SDK 切换说明见 -[鸿蒙指南](doc/ohos.zh-CN.md#缓存与工具链切换)。 - --- ## 运行指标与设计约束 diff --git a/doc/ohos.md b/doc/ohos.md deleted file mode 100644 index acb3e05..0000000 --- a/doc/ohos.md +++ /dev/null @@ -1,238 +0,0 @@ -# OpenHarmony / HarmonyOS native support - -[English](ohos.md) | [简体中文](ohos.zh-CN.md) - -Build and bundle `ech_http` through Flutter-OH Native Assets on `arm64` -(phones and tablets) and `x64`. The existing `EchClient`, ECH, certificate -validation, proxy, gzip, and streaming APIs remain available. See the -verification scope below for the checks performed on each architecture. - -## Requirements - -- Dart >=3.10, CMake >=3.22, and Ninja. `ech_http` consumers do not need Python - or Go to build the precompiled dependencies. -- An OpenHarmony / HarmonyOS native SDK containing `build/cmake/ohos.toolchain.cmake`. - The precompiled SDK and verified consumer use version 26.0.0.105 / API 26; - compatibility with lower API levels has not been verified. - `OHOS_NDK_HOME` may point to the native directory or its parent. - The HarmonyOS command-line tools layout - `HOS_SDK_HOME/default/openharmony/native` is also supported. -- Flutter-OH with OHOS Native Assets support for application builds. - Verified with CPF-Flutter `oh-3.41.9-release`, commit - `62357a93d653bf844f730bb1f4b7cc9e2139d14e`. - Earlier toolchains with only FFI plugin packaging are insufficient. - -The compatible `code_assets` and `hooks` version ranges accommodate Flutter-OH -3.41's `meta 1.17.0` pin. The verified consumer resolves `code_assets 1.2.1` -and `hooks 2.0.2`, without overriding Flutter's `meta` dependency. - -The verified Flutter-OH toolchain passes `target_os=linux` to third-party hooks -alongside its `native/llvm/bin/clang`. The hook identifies OHOS from that -compiler's SDK and uses the OHOS toolchain and sysroot, rather than Linux -prebuilt dependencies. Explicit `target_os=ohos` inputs are also recognized. -Installing an OHOS SDK alone does not redirect ordinary Linux builds. - -## Application setup - -Reference an `ech_http` version containing OHOS support from the Flutter-OH -application's `pubspec.yaml`. For development and verification, use a local -dependency: - -```yaml -dependencies: - ech_http: - path: /absolute/path/to/ech_http -``` - -Run `flutter pub get` and `flutter build hap` with Flutter-OH. -The hook normally locates the SDK from Flutter's compiler path. If that path -is unavailable, configure the target explicitly in the **application's** -`pubspec.yaml`: - -```yaml -hooks: - user_defines: - ech_http: - target: ohos - ohos_sdk: /absolute/path/to/openharmony/native - # Optional: share verified precompiled SDKs between builds. - binary_cache: /absolute/path/to/ech-http-sdks -``` - -The first build downloads the precompiled SDK pinned by version and SHA-256 -in `lib/src/build_support/dependencies.json`. After validating metadata and -per-file hashes, the hook compiles only `src/ech_http.cpp`. Consumers no longer -download or compile BoringSSL, curl, or zlib sources. Later builds reuse the -verified SDK cache, with the same integrity and repair checks as other platforms. - -The independent [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) -repository builds and publishes these SDKs, starting with -[v0.1.0](https://github.com/tignioj/libechhttp_ohos_build/releases/tag/v0.1.0). -Each archive includes matching headers, four static libraries, CMake targets, -licenses, per-file hashes, and build provenance. Maintainers update reviewed -pins with `python3 tool/update_prebuilt.py ohos=v0.1.0`; application hooks never -invoke that Python tool. - -OHOS links the C++ runtime statically and hides static library symbols. The HAP -does not need an extra `libc++_shared.so`, and TLS symbols are not exported to -other native plugins. - -## Standalone builds - -Check SDK download, bridge cross-compilation, and Native Assets output without -creating an application: - -```sh -export OHOS_NDK_HOME=/absolute/path/to/openharmony/native -dart pub get -dart run tool/check_native_build.dart ohos arm64 -dart run tool/check_native_build.dart ohos x64 -``` - -The checks use Flutter-OH's Linux compatibility input and OHOS compiler, and -download the corresponding precompiled SDK. Manifests are written to -`.dart_tool/verify/ohos-arm64-device/output.json` and -`.dart_tool/verify/ohos-x64-device/output.json`, with the library identified as -`package:ech_http/ech_http_bindings`. - -Maintainers rebuilding dependencies use the independent SDK repository's build -and publication workflow. Neither this package nor consumers contain an entry -point to compile those dependency sources. - -## Flutter device smoke app - -```sh -dart run tool/create_flutter_smoke.dart ohos -cd build/flutter_smoke -flutter pub get -flutter build hap --debug --no-codesign -``` - -The app's bundle name is `com.example.ech_http_smoke`, with phone and tablet -support. It discovers Cloudflare's ECH configuration over DoH, sends an HTTPS -request, and displays the status, `ECH accepted`, and backend version. Sign -with the normal Huawei debug signing workflow before installing on a device. -Success requires HTTP 200 and `ECH accepted: true`. - -The unsigned HAP is at -`build/flutter_smoke/build/ohos/hap/entry-default-unsigned.hap`, relative to -the repository root. The verified Flutter-OH macOS Release AOT compiler is -Intel-only, so Release builds on Apple Silicon require Rosetta. Debug smoke -builds do not use that AOT compiler. - -## Verification scope - -### Precompiled SDK device rerun (2026-10-17) - -A signed Debug HAP using the published precompiled SDK was reinstalled and -launched on MatePad mini (MLR-AL10, API 26). The tested `ech_http` implementation -was commit `0a94849d7e1efba33abafd3a2529cf8dca4a2268`, with the `v0.1.0` arm64 SDK -built from `6ebb0d4b4617115f4a2cc7f1b324d861e222c9a2`. The SDK archive SHA-256 was -`986aaea773733301b66a1f94a1986f71b1a31f54e89c4909fcc2fd4b0ea982fc`. - -The app was rebuilt with Flutter-OH 3.41.9+ohos-1.0.2, Dart 3.11.5, and HarmonyOS -SDK 26.0.0.105 after clearing its `.dart_tool` and Flutter build outputs. Local -checks verified the downloaded archive digest, precompiled static-library link -paths, HAP arm64 library, and Native Assets mapping. The packaged library exactly -matched the newly built bridge after `llvm-strip`; its sole ELF dynamic dependency -was `libc.so`. The HAP was signed with the existing local debug identity, passed -signature verification, and was successfully installed over the test app. - -At 14:05:48 (Asia/Shanghai), the device returned the unique marker for this run. -Native Assets loading and FFI calls succeeded, including the backend version. -After discovering ECH configuration over DoH, the request to -`https://crypto.cloudflare.com/cdn-cgi/trace` returned: - -```text -ECH_HTTP_PREBUILT_V0_1_0_0a94849: libcurl/8.22.0 BoringSSL zlib/1.3.2 -HTTP 200 -ECH accepted: true -tls=TLSv1.3 -sni=encrypted -``` - -The raw local capture is `build/prebuilt-ohos-runtime-smoke.log`; provenance and -assertion results are in `build/prebuilt-device-result.json`. These generated -files are not published with the package. This rerun covers arm64 signed Debug -HAP loading and live HTTPS/ECH. It does not cover signed Release HAP execution, -compatibility below API 26, x64 devices, or individual OHOS device regression -checks for proxies, gzip, cancellation, and isolate shutdown. Desktop coverage -for these features does not establish OHOS device verification. - -### Initial source-build record - -The initial source-build verification on 2026-10-17 used a macOS arm64 host, Dart 3.13.5, -and OpenHarmony native SDK 6.0.0.47 (API 20). The Flutter HAP used -Flutter-OH 3.41.9+ohos-1.0.2, its bundled Dart 3.11.5, and HarmonyOS SDK -26.0.0.105. The connected MatePad mini runs HarmonyOS API 26. - -| Check | Result | -| --- | --- | -| arm64 native library and smoke executable | Cross-compilation passed | -| x64 native library and smoke executable | Cross-compilation passed; no x64 device attached | -| Flutter-OH compatibility input | Build hook produced the Native Assets manifest | -| ELF dynamic dependencies | Only `libc.so`; no additional TLS, zlib, or C++ shared library | -| macOS offline unit and native regression tests | Passed; public-network tests are opt-in | -| Flutter-OH consumer dependency resolution | Passed with Flutter's `meta 1.17.0` pin | -| Flutter smoke app analysis | Passed | -| Debug HAP packaging | Passed with arm64 `libech_http.so` and Native Assets mapping | -| Local Release HAP build | Native library and Dart compilation passed; AOT requires Rosetta, absent on the verified host | -| Bare ELF execution on MatePad mini | Rejected with `Permission denied` before request code ran | -| Initial source-build signed Debug HAP on MatePad mini | Native library loaded and FFI symbols resolved | -| Initial source-build device HTTPS / ECH request | HTTP 200, accepted ECH, `sni=encrypted`, and `tls=TLSv1.3` | - -The earlier source-build signed HAP's device log contains: - -```text -ECH_HTTP_SMOKE: libcurl/8.22.0 BoringSSL zlib/1.3.2 -HTTP 200 -ECH accepted: true -tls=TLSv1.3 -sni=encrypted -``` - -The local capture is stored in `build/ohos-runtime-smoke.log` (a generated -file, not part of the package). Device verification covers Native Assets -integration and public HTTPS/ECH. gzip, proxies, cancellation, and isolate -shutdown have desktop regression coverage but have not each been repeated -on an OHOS device. x64 has build coverage only. Release HAP and remote CI -verification were not part of the initial local device run. The separate -precompiled SDK device rerun is recorded above. - -The dependency repository's [initial SDK CI](https://github.com/tignioj/libechhttp_ohos_build/actions/runs/39006660450) -verified arm64/x64 dependency and packaged consumer links, ELF architecture, -dynamic dependencies, archive and file digests, and source provenance. -It does not execute target device code. - -One local macOS arm64 check on the same date downloaded this Release into an -empty SDK cache and built the bridge in 6.7 seconds for arm64 and 7.0 seconds -for x64. These observations include downloading and building, depend on the -network and build host, and are not a timing guarantee for other environments. - -## Caching and Dart SDK changes - -OHOS shares `preparePrebuilt` with the other platforms. The target and archive -SHA-256 identify the cache directory. ZIP digests, SDK metadata, and per-file -hashes are checked; modified extracted files are repaired from the verified -archive. New archive pins use a new cache directory. `binary_cache` paths are -relative to the consuming `pubspec.yaml`; a complete cache works offline. -The hook resets bridge CMake metadata when the package source moves. - -When switching between standard Dart and Flutter-OH's bundled Dart, -regenerate the application's `.dart_tool` and run `flutter pub get`. -Otherwise a cached `hook.dill` may produce -`Invalid kernel binary format version`. CI keeps package tools on their -original Dart SDK and gives the Flutter-OH consumer its own `.dart_tool`. - -```sh -dart test test/ohos_build_test.dart test/prebuilt_test.dart -``` - -Independent SDK CI builds and link-checks both architectures, validates ELF -properties, and verifies complete archives. Package CI downloads published -SDKs, builds only the bridge, and checks arm64/x64 hook outputs plus unsigned -Release HAP contents and Native Assets mapping with pinned Flutter-OH. -Neither pipeline replaces signed device execution. x64 still has no device -coverage. Proxy, gzip, cancellation, and isolate shutdown have desktop tests -but have not each been repeated on an OHOS device. The precompiled SDK device -rerun above is manual verification, separate from automated CI. diff --git a/doc/ohos.zh-CN.md b/doc/ohos.zh-CN.md deleted file mode 100644 index 583dc51..0000000 --- a/doc/ohos.zh-CN.md +++ /dev/null @@ -1,213 +0,0 @@ -# OpenHarmony / HarmonyOS 原生适配 - -[English](ohos.md) | [简体中文](ohos.zh-CN.md) - -支持通过 Flutter-OH 的 Native Assets 构建和打包 `ech_http`。 -保留现有 `EchClient`、ECH、证书校验、代理、gzip 和流式响应 API。 -支持 `arm64`(手机、平板)和 `x64`;各架构的验证结果见下文。 - -## 构建要求 - -- Dart >=3.10、CMake >=3.22、Ninja;依赖库已预编译,无需为 `ech_http` 安装 Python 或 Go。 -- OpenHarmony / HarmonyOS native SDK,包含 `build/cmake/ohos.toolchain.cmake`。 - 预编译 SDK 使用 26.0.0.105 / API 26;当前消费方也在该版本验证,低 API 兼容性未验证。 - 可将 `OHOS_NDK_HOME` 指向 `native` 本身或其父目录;也支持 HarmonyOS - 命令行工具的 `HOS_SDK_HOME/default/openharmony/native` 布局。 -- 应用构建需要已支持 OHOS Native Assets 的 Flutter-OH。 - 本次核对的 CPF-Flutter `oh-3.41.9-release` 提交 - `62357a93d653bf844f730bb1f4b7cc9e2139d14e` 已包含该功能。 - 仅有旧版 FFI 插件打包支持的 Flutter-OH 不满足要求。 - -包允许使用 `code_assets` / `hooks` 的兼容版本范围,解决 Flutter-OH -3.41 固定 `meta 1.17.0` 与 `hooks 2.2` 的依赖冲突。实际测试应用解析为 -`code_assets 1.2.1`、`hooks 2.0.2`,无需覆盖 Flutter SDK 的 `meta` 版本。 - -Flutter-OH 当前向第三方 hook 传递 `target_os=linux`,同时提供 OHOS 的 -`native/llvm/bin/clang`。构建钩子根据编译器所属 SDK 识别鸿蒙目标,使用 -OHOS toolchain、sysroot 和架构编译,**不会使用 Linux 预编译依赖**。 -将来直接传递 `target_os=ohos` 的工具链也可以识别。 - -## 应用接入 - -在 Flutter-OH 应用的 `pubspec.yaml` 中引用包含 OHOS 支持的 `ech_http`。 -开发与验证时可使用本地依赖: - -```yaml -dependencies: - ech_http: - path: /absolute/path/to/ech_http -``` - -使用 Flutter-OH 运行 `flutter pub get` 和 `flutter build hap`。 -正常情况下自动从 Flutter 提供的编译器路径定位 native SDK。 -若工具链没有提供该路径,可在**应用**的 `pubspec.yaml` 中明确配置: - -```yaml -hooks: - user_defines: - ech_http: - target: ohos - ohos_sdk: /absolute/path/to/openharmony/native - # 可选:跨构建复用经过校验的预编译 SDK。 - binary_cache: /absolute/path/to/ech-http-sdks -``` - -首次构建下载 `lib/src/build_support/dependencies.json` 固定版本与 SHA-256 的 -预编译依赖 SDK,校验元数据及逐文件摘要后,本地仅编译 `src/ech_http.cpp` -胶水代码。消费方不再下载、编译 BoringSSL、curl、zlib 源码。 -后续构建复用已校验的 SDK 缓存;下载与缓存修复机制和其他平台一致。 - -预编译 SDK 由独立仓库 -[libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) 构建和发布, -首版为 [v0.1.0](https://github.com/tignioj/libechhttp_ohos_build/releases/tag/v0.1.0)。 -包含匹配头文件、四个静态库、CMake 配置、许可证和构建来源元数据。 -维护者更新 SDK 时运行 `python3 tool/update_prebuilt.py ohos=v0.1.0`; -该 Python 工具不会由应用构建钩子调用。 - -OHOS 使用静态 C++ 运行库,并隐藏静态依赖符号。HAP 不必额外打包 -`libc++_shared.so`,也不会把 TLS 库的符号暴露给其他播放器插件。 - -## 独立编译与检查 - -不必先创建 Flutter 应用即可检查 SDK 下载、胶水代码交叉编译和 Native Assets 输出: - -```sh -export OHOS_NDK_HOME=/absolute/path/to/openharmony/native -dart pub get -dart run tool/check_native_build.dart ohos arm64 -dart run tool/check_native_build.dart ohos x64 -``` - -该检查使用与 Flutter-OH 一致的 Linux 兼容输入和 OHOS 编译器,下载对应架构的 -预编译 SDK。清单位于 `.dart_tool/verify/ohos-arm64-device/output.json` -或 `.dart_tool/verify/ohos-x64-device/output.json`,包含 -`package:ech_http/ech_http_bindings` 对应的鸿蒙 `.so`。 - -维护者如需重建大型依赖,应在独立 SDK 仓库执行构建与发布流程; -应用项目和本包内不再提供源码依赖编译入口。 - -## Flutter 真机测试包 - -使用 Flutter-OH 创建独立的 ECH 测试应用: - -```sh -dart run tool/create_flutter_smoke.dart ohos -cd build/flutter_smoke -flutter pub get -flutter build hap --debug --no-codesign -``` - -应用包名为 `com.example.ech_http_smoke`,支持手机、平板。 -启动后通过 DoH 获取 Cloudflare ECH 配置,发起 HTTPS 请求,并显示 -HTTP 状态、`ECH accepted` 和后端版本。使用正常的华为调试签名后安装。 -成功标准是 HTTP 200、`ECH accepted: true`,不能只根据页面打开判断成功。 - -包路径为 `build/flutter_smoke/build/ohos/hap/entry-default-unsigned.hap` -(相对于本仓库根目录)。当前 Flutter-OH 的 macOS Release AOT 编译器 -为 Intel 架构;Apple Silicon 上的 Release 构建需要 Rosetta。 -Debug 测试包不依赖这个 AOT 编译器。 - -## 验证范围 - -### 预编译 SDK 真机复测(2026-10-17) - -在 MatePad mini(MLR-AL10,API 26)上重新安装并启动了使用预编译 SDK 的签名 -Debug HAP。本次测试的 `ech_http` 实现提交为 -`0a94849d7e1efba33abafd3a2529cf8dca4a2268`,依赖使用已发布的 `v0.1.0` -arm64 SDK(构建提交 `6ebb0d4b4617115f4a2cc7f1b324d861e222c9a2`)。 -SDK 归档 SHA-256 为 -`986aaea773733301b66a1f94a1986f71b1a31f54e89c4909fcc2fd4b0ea982fc`。 - -使用 Flutter-OH 3.41.9+ohos-1.0.2、Dart 3.11.5 和 HarmonyOS SDK 26.0.0.105, -清理应用 `.dart_tool` 与 Flutter 构建输出后重新构建。本地确认下载归档摘要、 -预编译静态库链接路径、HAP 中的 arm64 库和 Native Assets 映射;包内库与本次 -胶水代码构建产物经 `llvm-strip` 后完全一致,ELF 动态依赖仅为 `libc.so`。 -HAP 使用已有的本机调试身份签名并通过签名验证,随后成功覆盖安装到设备。 - -设备于 14:05:48(Asia/Shanghai)返回本次专用日志标记。Native Assets 库加载及 -FFI 调用成功,后端版本正常返回;通过 DoH 获取 ECH 配置后,请求 -`https://crypto.cloudflare.com/cdn-cgi/trace` 返回: - -```text -ECH_HTTP_PREBUILT_V0_1_0_0a94849: libcurl/8.22.0 BoringSSL zlib/1.3.2 -HTTP 200 -ECH accepted: true -tls=TLSv1.3 -sni=encrypted -``` - -原始设备日志保存在本地 `build/prebuilt-ohos-runtime-smoke.log`,产物来源与断言结果 -保存在 `build/prebuilt-device-result.json`;两者均为生成文件,不随包发布。 -本次为 arm64 签名 Debug HAP 的加载和公网 HTTPS/ECH 验证,不包含 Release HAP -真机运行、低于 API 26 的兼容性、x64 设备,以及代理、gzip、取消请求和 isolate -退出的逐项鸿蒙真机回归。这些功能的桌面测试覆盖不等同于鸿蒙真机验证。 - -### 初始源码构建记录 - -2026-10-17 初始源码构建版本的本地检查使用 macOS arm64 构建机、Dart 3.13.5、OpenHarmony -native SDK 6.0.0.47(API 20)。Flutter HAP 使用 Flutter-OH -3.41.9+ohos-1.0.2、其内置 Dart 3.11.5 和 HarmonyOS SDK 26.0.0.105。 -连接的 MatePad mini 为 HarmonyOS API 26。 - -| 检查 | 结果 | -| --- | --- | -| arm64 原生库及测试程序交叉编译 | 通过 | -| x64 原生库及测试程序交叉编译 | 通过;未连接 x64 设备 | -| Flutter-OH 兼容输入的构建钩子 | 通过,生成 `package:ech_http/ech_http_bindings` 清单 | -| ELF 动态依赖检查 | 仅依赖 `libc.so`,没有额外的 TLS、zlib、C++ 动态库 | -| macOS Dart 单元和原生回归测试 | 通过;网络 live 测试需单独启用 | -| Flutter-OH 3.41.9+ohos-1.0.2 应用依赖解析 | 通过,兼容 Flutter 固定的 `meta 1.17.0` | -| Flutter 测试应用静态分析 | 通过 | -| Debug HAP 构建及库打包 | 通过,包内包含 arm64 `libech_http.so` 与 Native Assets 映射 | -| 本机 Release HAP 构建 | 原生库与 Dart 编译通过;AOT 编译器需要本机尚未安装的 Rosetta | -| MatePad mini 上执行裸 ELF | 系统返回 `Permission denied`,未执行到请求代码 | -| 初始源码构建版本的签名 Debug HAP 真机运行 | 通过,Native Assets 中的 arm64 库正常加载并解析 FFI 符号 | -| 初始源码构建版本的 MatePad mini HTTPS / ECH 请求 | 通过,DoH 获取 ECH 配置后返回 HTTP 200、`ECH accepted: true`,服务端返回 `sni=encrypted`、`tls=TLSv1.3` | - -此前源码构建版本的真机日志如下(来自正常签名安装的 Flutter HAP): - -```text -ECH_HTTP_SMOKE: libcurl/8.22.0 BoringSSL zlib/1.3.2 -HTTP 200 -ECH accepted: true -tls=TLSv1.3 -sni=encrypted -``` - -本地验证记录保存在 `build/ohos-runtime-smoke.log`。本次已验证 Flutter -Native Assets 集成和公网 HTTPS/ECH;gzip、代理、取消请求、isolate 退出 -等行为已覆盖桌面回归测试,尚未在鸿蒙真机上逐项复测。 -x64 仅完成交叉编译和构建钩子检查。初次本地真机验证未包含 Release HAP -与远程 CI。预编译 SDK 版本的独立真机复测结果见上文。 - -预编译依赖仓库的[首版 CI](https://github.com/tignioj/libechhttp_ohos_build/actions/runs/39006660450) -已验证 arm64、x64 的依赖与独立消费方链接、ELF 架构、动态依赖、归档摘要、 -逐文件摘要及来源提交。该 CI 不执行目标设备代码。 - -同日 macOS arm64 本地的一次检查中,从空的 SDK 缓存下载该 Release 并构建胶水代码, -arm64 耗时 6.7 秒、x64 耗时 7.0 秒。该观察包含下载和构建,受网络与构建机影响, -不代表所有环境的固定耗时。 - -## 缓存与工具链切换 - -OHOS 和其他平台共用 `preparePrebuilt`:以目标架构和归档 SHA-256 区分缓存, -核验 ZIP、SDK 元数据及逐文件摘要;解压文件损坏时从已校验的归档修复。 -更换版本或归档摘要后自动使用新的缓存目录,不会把旧依赖产物混入新 SDK。 -`binary_cache` 路径相对于消费方 `pubspec.yaml`。缓存完整时可离线构建。 -构建钩子的胶水代码会在包源码路径变更时重置 CMake 元数据。 - -切换标准 Dart 与 Flutter-OH 内置 Dart 时,应重新生成应用的 `.dart_tool` -并运行 `flutter pub get`,避免旧 SDK 编译的 `hook.dill` 导致 -`Invalid kernel binary format version`。CI 中包级检查始终使用原 Dart SDK, -Flutter-OH 测试应用使用自身独立的 `.dart_tool` 和内置 Dart。 - -```sh -dart test test/ohos_build_test.dart test/prebuilt_test.dart -``` - -独立 SDK CI 验证两个架构的大型依赖编译、链接、ELF 和归档完整性; -本包 CI 从已发布 Release 下载 SDK,仅编译胶水代码,并检查 arm64/x64 -构建钩子输出及固定 Flutter-OH 工具链的未签名 Release HAP 打包和映射。 -两类 CI 都不替代签名 HAP 的真机运行。x64 尚未进行真机验证;代理、gzip、 -取消请求和 isolate 退出尚未在鸿蒙真机逐项复测。上述预编译 SDK 真机复测 -为手动设备验证,不属于自动化 CI。 diff --git a/doc/releasing.md b/doc/releasing.md index 44bf841..9a9a6de 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -91,21 +91,22 @@ Set `ECH_PROXY`, `ECH_CONFIG_DOMAIN`, or `ECH_ADDRESSES` in your environment to ## 3. Precompiled Dependency SDK Maintenance -`ech_http` pins versioned precompiled SDKs for 14 target architectures across four public build repositories: +`ech_http` pins versioned precompiled SDKs for 16 target architectures across five public build repositories: - [libechhttp-win32-build](https://github.com/ech-research/libechhttp-win32-build/releases) (Windows: x64, arm64, ia32) - [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build/releases) (macOS / iOS: x64, arm64) - [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build/releases) (Android: arm64-v8a, armeabi-v7a, x86_64, x86) - [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build/releases) (Linux: x64, arm64) +- [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build/releases) (OpenHarmony / HarmonyOS: arm64, x64) ### Upgrading Precompiled SDKs When dependency versions (such as libcurl or BoringSSL) or compiler flags change: -1. Trigger and publish new GitHub Releases in the four platform build repositories. +1. Trigger and publish new GitHub Releases in the relevant platform build repositories. 2. In the `ech_http` repository, run the maintainer automation tool with the new release tags: ```sh - python tool/update_prebuilt.py win32=v0.1.1 darwin=v0.1.2 android=v0.1.1 linux=v0.1.1 + python tool/update_prebuilt.py win32=v0.1.1 darwin=v0.1.2 android=v0.1.1 linux=v0.1.1 ohos=v0.1.0 ``` *(Requires Python 3.9+ and an authenticated GitHub CLI `gh`)*. 3. Review the modified pins and SHA-256 digests in `lib/src/build_support/dependencies.json`. @@ -140,6 +141,7 @@ The repository includes a GitHub Actions CI workflow (`.github/workflows/ci.yml` - macOS desktop runners (Intel x64 and Apple Silicon arm64) - Android APK packaging (Flutter build across arm64, arm, x64, x86) - iOS build targets (bridge build and Flutter release build with `--no-codesign`) +- OHOS bridge builds (arm64/x64) and unsigned Flutter Release HAP packaging Run or verify the CI pipeline before publishing. See [doc/verification.md](verification.md) for detailed verification coverage. diff --git a/doc/releasing.zh-CN.md b/doc/releasing.zh-CN.md index 6893e29..1f3d743 100644 --- a/doc/releasing.zh-CN.md +++ b/doc/releasing.zh-CN.md @@ -91,21 +91,22 @@ dart run example/ech_http_example.dart https://crypto.cloudflare.com/cdn-cgi/tra ## 3. 预编译原生 SDK 的更新与维护 -`ech_http` 依赖 4 个独立的公开构建仓库为 14 个目标平台与架构打包发布预编译 SDK: +`ech_http` 依赖 5 个独立的公开构建仓库为 16 个目标平台与架构打包发布预编译 SDK: - [libechhttp-win32-build](https://github.com/ech-research/libechhttp-win32-build/releases)(Windows: x64, arm64, ia32) - [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build/releases)(macOS / iOS: x64, arm64) - [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build/releases)(Android: arm64-v8a, armeabi-v7a, x86_64, x86) - [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build/releases)(Linux: x64, arm64) +- [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build/releases)(OpenHarmony / HarmonyOS: arm64, x64) ### 升级预编译依赖版本 当 libcurl、BoringSSL 源码更新或编译参数调整时: -1. 在上述 4 个构建仓库中触发 GitHub Actions 并发布新的 Release 版本。 +1. 在相应平台的构建仓库中触发 GitHub Actions 并发布新的 Release 版本。 2. 在本项目根目录下运行维护脚本,填入经过评审的新 Release 标签: ```sh - python tool/update_prebuilt.py win32=v0.1.1 darwin=v0.1.2 android=v0.1.1 linux=v0.1.1 + python tool/update_prebuilt.py win32=v0.1.1 darwin=v0.1.2 android=v0.1.1 linux=v0.1.1 ohos=v0.1.0 ``` *(要求本地装有 Python 3.9+ 且具备经过身份授权的 GitHub CLI `gh`)*。 3. 检查并核对 `lib/src/build_support/dependencies.json` 中自动更新的下载链接与 SHA-256 散列值。 @@ -140,6 +141,7 @@ dart run example/ech_http_example.dart https://crypto.cloudflare.com/cdn-cgi/tra - macOS 环境(Intel x64 与 Apple Silicon arm64 构建及测试) - Android 构建(Flutter 打包 arm64、arm、x64、x86 APK) - iOS 构建(C++ 桥接编译及 Flutter `--no-codesign` 打包) +- OHOS 构建(arm64/x64 桥接编译及 Flutter 未签名 Release HAP 打包) 发布新版本前,确保 CI 所有任务绿灯通过。具体覆盖范围详见 [doc/verification.zh-CN.md](verification.zh-CN.md)。 diff --git a/doc/verification.md b/doc/verification.md index 62cbd23..8fb9adb 100644 --- a/doc/verification.md +++ b/doc/verification.md @@ -42,7 +42,7 @@ We distinguish between three tiers of verification: | **Android** | `arm64-v8a` | **Tier 1** | Physical device run (API 35), live ECH handshake, authenticated retry, connection reuse | Non-standard custom Android ROMs | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK packaging for both ABIs | Runtime execution on physical 32-bit ARM / x86_64 devices | | **Android** | `x86` | **Tier 2** | NDK bridge build and link check in CI | 32-bit x86 Android runtime execution | -| **OpenHarmony / HarmonyOS** | arm64, x64 | **Tier 2** | arm64/x64 precompiled SDK and bridge builds; on 2026-10-17 a signed Debug HAP using SDK v0.1.0 loaded the native library on MatePad mini (API 26), with HTTP 200, accepted ECH, `sni=encrypted`, and TLS 1.3 | See [OHOS scope](ohos.md); full suite, signed Release HAP, and lower API compatibility not verified on OHOS, no x64 device attached | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | **Tier 2** | Precompiled SDK builds; signed Debug HAP on MatePad mini (API 26): native loading, HTTP 200 and accepted ECH | Signed Release HAP, x64 devices, lower APIs, and proxy/gzip/cancellation/isolate device regressions unverified | | **Web** | All | **Tier 3** | Not supported by this native C++ FFI architecture | N/A | --- diff --git a/doc/verification.zh-CN.md b/doc/verification.zh-CN.md index 39ec85f..657a197 100644 --- a/doc/verification.zh-CN.md +++ b/doc/verification.zh-CN.md @@ -37,7 +37,7 @@ Flutter Android APK、iOS 未签名 Release 应用、iOS 模拟器 Debug 应用 | **Android** | `arm64-v8a` | **Tier 1** | 真实设备运行(API 35),公网 ECH 握手、认证重试、连接复用均通过 | 各类深度定制的非标准第三方 Android ROM | | **Android** | `armeabi-v7a`, `x86_64` | **Tier 2** | Flutter Debug APK 多 ABI 打包测试通过 | 32 位 ARM / x86_64 物理机运行时执行 | | **Android** | `x86` | **Tier 2** | CI 中 NDK 桥接构建与链接核验通过 | 32 位 x86 Android 实体机运行时执行 | -| **OpenHarmony / HarmonyOS** | arm64、x64 | **Tier 2** | arm64/x64 预编译 SDK 与胶水代码构建;2026-10-17 使用 v0.1.0 SDK 的签名 Debug HAP 在 MatePad mini(API 26)成功加载原生库,返回 HTTP 200、ECH 被接受、`sni=encrypted`、TLS 1.3 | 真机验证范围见[鸿蒙说明](ohos.zh-CN.md);完整功能套件、签名 Release HAP 和低 API 兼容性未在鸿蒙复测,未连接 x64 设备 | +| **OpenHarmony / HarmonyOS** | `arm64`, `x64` | **Tier 2** | 预编译 SDK 构建;MatePad mini(API 26)签名 Debug HAP 原生库加载、HTTP 200 和 ECH 通过 | 未验证签名 Release HAP、x64 真机、低 API 及代理/gzip/取消请求/isolate 退出的真机回归 | | **Web** | 全部架构 | **Tier 3** | C++ FFI 原生引擎架构不支持 Web | 不适用 | --- From d42e7fc49061ac50dd08bcad77991732c3e3e3ea Mon Sep 17 00:00:00 2001 From: tignioj <859897994@qq.com> Date: Sat, 17 Oct 2026 17:19:58 +0800 Subject: [PATCH 8/8] build(ohos): use the organization SDK repository --- doc/releasing.md | 2 +- doc/releasing.zh-CN.md | 2 +- doc/verification.md | 2 +- doc/verification.zh-CN.md | 2 +- lib/src/build_support/dependencies.json | 4 ++-- tool/update_prebuilt.py | 5 +---- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/releasing.md b/doc/releasing.md index 9a9a6de..5d2b053 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -97,7 +97,7 @@ Set `ECH_PROXY`, `ECH_CONFIG_DOMAIN`, or `ECH_ADDRESSES` in your environment to - [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build/releases) (macOS / iOS: x64, arm64) - [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build/releases) (Android: arm64-v8a, armeabi-v7a, x86_64, x86) - [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build/releases) (Linux: x64, arm64) -- [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build/releases) (OpenHarmony / HarmonyOS: arm64, x64) +- [libechhttp-ohos-build](https://github.com/ech-research/libechhttp-ohos-build/releases) (OpenHarmony / HarmonyOS: arm64, x64) ### Upgrading Precompiled SDKs diff --git a/doc/releasing.zh-CN.md b/doc/releasing.zh-CN.md index 1f3d743..de48830 100644 --- a/doc/releasing.zh-CN.md +++ b/doc/releasing.zh-CN.md @@ -97,7 +97,7 @@ dart run example/ech_http_example.dart https://crypto.cloudflare.com/cdn-cgi/tra - [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build/releases)(macOS / iOS: x64, arm64) - [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build/releases)(Android: arm64-v8a, armeabi-v7a, x86_64, x86) - [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build/releases)(Linux: x64, arm64) -- [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build/releases)(OpenHarmony / HarmonyOS: arm64, x64) +- [libechhttp-ohos-build](https://github.com/ech-research/libechhttp-ohos-build/releases)(OpenHarmony / HarmonyOS: arm64, x64) ### 升级预编译依赖版本 diff --git a/doc/verification.md b/doc/verification.md index 8fb9adb..e68d6f4 100644 --- a/doc/verification.md +++ b/doc/verification.md @@ -57,7 +57,7 @@ Precompiled dependency SDKs (statically built libcurl 8.22.0 + BoringSSL + zlib | [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build) | `v0.1.2` | macOS / iOS: `x64`, `arm64` (Device + Simulator) | | [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build) | `v0.1.1` | Android: `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | | [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build) | `v0.1.1` | Linux: `x64`, `arm64` | -| [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) | `v0.1.0` | OpenHarmony / HarmonyOS: `arm64`, `x64` (SDK API 26) | +| [libechhttp-ohos-build](https://github.com/ech-research/libechhttp-ohos-build) | `v0.1.0` | OpenHarmony / HarmonyOS: `arm64`, `x64` (SDK API 26) | All release archives and their SHA-256 digests are pinned in [lib/src/build_support/dependencies.json](../lib/src/build_support/dependencies.json). diff --git a/doc/verification.zh-CN.md b/doc/verification.zh-CN.md index 657a197..24ee141 100644 --- a/doc/verification.zh-CN.md +++ b/doc/verification.zh-CN.md @@ -52,7 +52,7 @@ Flutter Android APK、iOS 未签名 Release 应用、iOS 模拟器 Debug 应用 | [libechhttp-darwin-build](https://github.com/ech-research/libechhttp-darwin-build) | `v0.1.2` | macOS / iOS: `x64`, `arm64`(真机 + 模拟器) | | [libechhttp-android-build](https://github.com/ech-research/libechhttp-android-build) | `v0.1.1` | Android: `arm64-v8a`, `armeabi-v7a`, `x86_64`, `x86` | | [libechhttp-linux-build](https://github.com/ech-research/libechhttp-linux-build) | `v0.1.1` | Linux: `x64`, `arm64` | -| [libechhttp_ohos_build](https://github.com/tignioj/libechhttp_ohos_build) | `v0.1.0` | OpenHarmony / HarmonyOS: `arm64`, `x64` (SDK API 26) | +| [libechhttp-ohos-build](https://github.com/ech-research/libechhttp-ohos-build) | `v0.1.0` | OpenHarmony / HarmonyOS: `arm64`, `x64` (SDK API 26) | 每个 SDK 压缩包的 SHA-256 散列均固化在 [lib/src/build_support/dependencies.json](../lib/src/build_support/dependencies.json) 中。 diff --git a/lib/src/build_support/dependencies.json b/lib/src/build_support/dependencies.json index 18ca26d..39ca67c 100644 --- a/lib/src/build_support/dependencies.json +++ b/lib/src/build_support/dependencies.json @@ -67,12 +67,12 @@ }, "ohos-arm64": { "release": "v0.1.0", - "url": "https://github.com/tignioj/libechhttp_ohos_build/releases/download/v0.1.0/libechhttp-deps-v0.1.0-ohos-arm64.zip", + "url": "https://github.com/ech-research/libechhttp-ohos-build/releases/download/v0.1.0/libechhttp-deps-v0.1.0-ohos-arm64.zip", "sha256": "986aaea773733301b66a1f94a1986f71b1a31f54e89c4909fcc2fd4b0ea982fc" }, "ohos-x64": { "release": "v0.1.0", - "url": "https://github.com/tignioj/libechhttp_ohos_build/releases/download/v0.1.0/libechhttp-deps-v0.1.0-ohos-x64.zip", + "url": "https://github.com/ech-research/libechhttp-ohos-build/releases/download/v0.1.0/libechhttp-deps-v0.1.0-ohos-x64.zip", "sha256": "240eb7c8eb2660302f5e73a2fc28c7a43326874c10522a474034cabcd7953bdc" }, "windows-arm64": { diff --git a/tool/update_prebuilt.py b/tool/update_prebuilt.py index b973e8a..bf5ee51 100644 --- a/tool/update_prebuilt.py +++ b/tool/update_prebuilt.py @@ -14,9 +14,6 @@ 'ohos': ['ohos-arm64', 'ohos-x64'], } -# Update this mapping after an authorized transfer to ech-research. -REPOSITORIES = {'ohos': 'tignioj/libechhttp_ohos_build'} - def main(): if not sys.argv[1:]: @@ -28,7 +25,7 @@ def main(): family, tag = item.split('=', 1) if family not in TARGETS or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+', tag): raise SystemExit(f'Invalid family or version: {item}') - repo = REPOSITORIES.get(family, f'ech-research/libechhttp-{family}-build') + repo = f'ech-research/libechhttp-{family}-build' release = json.loads(subprocess.check_output([ 'gh', 'api', f'repos/{repo}/releases/tags/{tag}', ], text=True, encoding='utf-8'))