diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 1b81c39..495681f 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -25,18 +25,32 @@ jobs: runner: macos-15 executable_suffix: "" binary_dir: build-ci/bin + cmake_arch: "" - id: macos-x64 runner: macos-15-intel executable_suffix: "" binary_dir: build-ci/bin + cmake_arch: "" - id: windows-x64 runner: windows-2022 executable_suffix: ".exe" binary_dir: build-ci/bin/Release + cmake_arch: x64 + - id: windows-arm64 + runner: windows-11-arm + executable_suffix: ".exe" + binary_dir: build-ci/bin/Release + cmake_arch: ARM64 - id: linux-x64 runner: ubuntu-24.04 executable_suffix: "" binary_dir: build-ci/bin + cmake_arch: "" + - id: linux-arm64 + runner: ubuntu-24.04-arm + executable_suffix: "" + binary_dir: build-ci/bin + cmake_arch: "" runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -54,7 +68,7 @@ jobs: - name: Configure without dependency network access (Windows) if: runner.os == 'Windows' shell: bash - run: cmake -S . -B build-ci -G "Visual Studio 17 2022" -A x64 -DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/.cache/dependencies" + run: cmake -S . -B build-ci -G "Visual Studio 17 2022" -A ${{ matrix.cmake_arch }} -DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/.cache/dependencies" - name: Build run: cmake --build build-ci --config Release --parallel - name: Test real PP-OCRv6 models diff --git a/.github/workflows/npm-release.yml b/.github/workflows/npm-release.yml index 108887e..9979ba3 100644 --- a/.github/workflows/npm-release.yml +++ b/.github/workflows/npm-release.yml @@ -4,9 +4,9 @@ on: workflow_dispatch: inputs: version: - description: Lockstep version for all six packages + description: Lockstep version for all release packages required: true - default: 0.3.0 + default: 0.3.1 type: string publish_to_registry: description: Publish the fully gated candidate to npm after preflight @@ -67,10 +67,20 @@ jobs: runner: ubuntu-24.04 executable_suffix: "" runtime_flavor: webgpu + - id: linux-arm64 + runner: ubuntu-24.04-arm + executable_suffix: "" + runtime_flavor: cpu - id: windows-x64 runner: windows-2022 executable_suffix: ".exe" runtime_flavor: webgpu + cmake_arch: x64 + - id: windows-arm64 + runner: windows-11-arm + executable_suffix: ".exe" + runtime_flavor: cpu + cmake_arch: ARM64 runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -146,7 +156,7 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: >- - cmake -S . -B build-npm -G "Visual Studio 17 2022" -A x64 + cmake -S . -B build-npm -G "Visual Studio 17 2022" -A ${{ matrix.cmake_arch }} "-DLIGHT_OCR_DEPENDENCY_CACHE_DIR=$env:GITHUB_WORKSPACE/.cache/dependencies" -DLIGHT_OCR_ONNXRUNTIME_FLAVOR=${{ matrix.runtime_flavor }} "-DLIGHT_OCR_WEBGPU_SDK_DIR=$env:GITHUB_WORKSPACE/dist/webgpu-sdk/${{ matrix.id }}" @@ -286,7 +296,7 @@ jobs: python tools/webgpu/package_bundle.py python tools/apple/package_bundle.py \ --qualification-report contracts/apple-provider-baselines.json - - name: Assemble and deterministically pack six packages + - name: Assemble and deterministically pack all release packages shell: bash run: | python tools/npm_release.py assemble \ @@ -337,6 +347,14 @@ jobs: runner: ubuntu-24.04 node: "24" native: linux-x64-gnu + - id: linux-arm64 + runner: ubuntu-24.04-arm + node: "22" + native: linux-arm64-gnu + - id: linux-arm64 + runner: ubuntu-24.04-arm + node: "24" + native: linux-arm64-gnu - id: windows-x64 runner: windows-2022 node: "22" @@ -345,6 +363,14 @@ jobs: runner: windows-2022 node: "24" native: win32-x64 + - id: windows-arm64 + runner: windows-11-arm + node: "22" + native: win32-arm64 + - id: windows-arm64 + runner: windows-11-arm + node: "24" + native: win32-arm64 runs-on: ${{ matrix.runner }} env: RELEASE_VERSION: ${{ inputs.version }} @@ -402,7 +428,7 @@ jobs: with: name: light-ocr-npm-${{ inputs.version }} path: dist/release - - name: Publish all six tarballs to an isolated registry + - name: Publish all release tarballs to an isolated registry shell: bash run: | npm install --global npm@11.0.0 verdaccio@6.8.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a334a..6991bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ This file records user-visible changes to `light-ocr`. Published artifact details and immutable hashes remain in [`docs/releases/`](docs/releases/). +## [0.3.1] - 2026-07-21 + +### Added + +- Added CPU-only prebuilt native runtimes for Linux arm64 (glibc) and Windows arm64, expanding the npm package set from four to six platform builds. Both new packages use the same PP-OCRv6 Small model and CPU result contract as the existing platforms. +- Added `@arcships/light-ocr-linux-arm64-gnu` and `@arcships/light-ocr-win32-arm64` as optional dependencies of the facade package, selected automatically by the Node.js loader on matching hosts. +- Added `ubuntu-24.04-arm` and `windows-11-arm` GitHub-hosted runners to the core CI and the npm release pipeline so both new builds compile, test, and pack on real arm64 hardware. + +### Changed + +- Extended the CMake ONNX Runtime CPU staging to select `runtimes/linux-arm64/native` and `runtimes/win-arm64/native` from the pinned NuGet package based on `CMAKE_SYSTEM_PROCESSOR`, instead of assuming x86_64. +- Made the npm release package-count invariant dynamic so adding platforms no longer requires editing a hard-coded count. + +### Notes + +- WebGPU acceleration remains limited to Linux x64 and Windows x64. The official ONNX Runtime WebGPU Plugin EP 0.1.0 does not ship a Linux arm64 binary, and Windows arm64 WebGPU has not passed the real-device Provider Gate required for a production-qualified release. Both arm64 packages are CPU-only in this version. + ## [0.3.0] - 2026-07-19 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index b91ce44..7a93216 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ if(APPLE AND (NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET OR "Minimum macOS deployment target" FORCE) endif() -project(light_ocr VERSION 0.3.0 LANGUAGES CXX) +project(light_ocr VERSION 0.3.1 LANGUAGES CXX) if(APPLE) enable_language(OBJCXX) diff --git a/README.md b/README.md index ba10739..1a94b10 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,16 @@ await engine.close(); ## Platform acceleration -The npm package provides the following four builds. The default `createEngine()` call uses Auto mode: +The npm package provides the following six builds. The default `createEngine()` call uses Auto mode: | Platform | Auto mode | | --- | --- | | macOS on Apple Silicon | Core ML on macOS 15+, then CPU | | macOS on Intel | CPU | | Linux x64 with glibc | WebGPU through Vulkan, then CPU | +| Linux arm64 with glibc | CPU | | Windows x64 | WebGPU through D3D12, then CPU | +| Windows arm64 | CPU | Applications that need explicit control can choose `auto`, `cpu`, `apple`, or `webgpu` through the [`execution` option](bindings/node/README.md#使用). diff --git a/README.zh-CN.md b/README.zh-CN.md index f50e4f3..81b1916 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -51,14 +51,16 @@ await engine.close(); ## 平台加速 -npm 包提供以下四个平台版本。默认的 `createEngine()` 使用 Auto 模式: +npm 包提供以下六个平台版本。默认的 `createEngine()` 使用 Auto 模式: | 平台 | Auto 模式 | | --- | --- | | macOS / Apple Silicon | macOS 15+ 优先使用 Core ML,然后使用 CPU | | macOS / Intel | CPU | | Linux x64 glibc | 通过 Vulkan 使用 WebGPU,然后使用 CPU | +| Linux arm64 glibc | CPU | | Windows x64 | 通过 D3D12 使用 WebGPU,然后使用 CPU | +| Windows arm64 | CPU | 需要明确控制时,可以通过 [`execution` 选项](bindings/node/README.md#使用)选择 `auto`、`cpu`、`apple` 或 `webgpu`。 diff --git a/bindings/node/CMakeLists.txt b/bindings/node/CMakeLists.txt index 0ad8c40..9d28370 100644 --- a/bindings/node/CMakeLists.txt +++ b/bindings/node/CMakeLists.txt @@ -90,14 +90,25 @@ elseif(APPLE) set(_light_ocr_node_platform_os "darwin") set(_light_ocr_node_platform_arch "x86_64") elseif(WIN32) - set(_light_ocr_node_platform_id "windows-x64") - set(_light_ocr_node_platform_os "win32") - set(_light_ocr_node_platform_arch "x86_64") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") + set(_light_ocr_node_platform_id "windows-arm64") + set(_light_ocr_node_platform_os "win32") + set(_light_ocr_node_platform_arch "arm64") + else() + set(_light_ocr_node_platform_id "windows-x64") + set(_light_ocr_node_platform_os "win32") + set(_light_ocr_node_platform_arch "x86_64") + endif() else() - set(_light_ocr_node_platform_id "linux-x64") set(_light_ocr_node_platform_os "linux") - set(_light_ocr_node_platform_arch "x86_64") set(_light_ocr_node_platform_libc "glibc") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + set(_light_ocr_node_platform_id "linux-arm64") + set(_light_ocr_node_platform_arch "arm64") + else() + set(_light_ocr_node_platform_id "linux-x64") + set(_light_ocr_node_platform_arch "x86_64") + endif() endif() set(_light_ocr_node_has_apple_provider 0) if(APPLE AND _light_ocr_node_platform_arch STREQUAL "arm64") diff --git a/bindings/node/README.md b/bindings/node/README.md index 7a4cfc8..fec9f8a 100644 --- a/bindings/node/README.md +++ b/bindings/node/README.md @@ -85,7 +85,7 @@ node --test --test-concurrency=1 bindings/node/test/adapter.test.cjs # 或:ctest --test-dir build-node -R '^light_ocr_node_tests$' --output-on-failure ``` -当前源码加载器不会搜索任意 cwd。开发构建优先接受 `LIGHT_OCR_NODE_BINARY`,发布构建则按 `process.platform`、`process.arch` 和 Linux libc 固定选择四个 `@arcships/light-ocr-` optional packages。facade 通过 model package 导出的 manifest 定位默认 bundle,并在进入 native addon 前核对 bundle ID。详见 [npm package 设计](../../docs/npm-packaging.md)。 +当前源码加载器不会搜索任意 cwd。开发构建优先接受 `LIGHT_OCR_NODE_BINARY`,发布构建则按 `process.platform`、`process.arch` 和 Linux libc 固定选择六个 `@arcships/light-ocr-` optional packages。facade 通过 model package 导出的 manifest 定位默认 bundle,并在进入 native addon 前核对 bundle ID。详见 [npm package 设计](../../docs/npm-packaging.md)。 ## 使用 diff --git a/bindings/node/js/load-native.cjs b/bindings/node/js/load-native.cjs index 4432101..8b7261b 100644 --- a/bindings/node/js/load-native.cjs +++ b/bindings/node/js/load-native.cjs @@ -17,6 +17,7 @@ function platformIdentity() { const identities = { 'darwin-arm64': { id: 'macos-arm64', os: 'darwin', architecture: 'arm64' }, 'darwin-x64': { id: 'macos-x64', os: 'darwin', architecture: 'x86_64' }, + 'win32-arm64': { id: 'windows-arm64', os: 'win32', architecture: 'arm64' }, 'win32-x64': { id: 'windows-x64', os: 'win32', architecture: 'x86_64' }, }; if (key === 'linux-x64') { @@ -30,6 +31,17 @@ function platformIdentity() { key, ); } + if (key === 'linux-arm64') { + const report = process.report?.getReport?.(); + if (report?.header?.glibcVersionRuntime) { + return { id: 'linux-arm64', os: 'linux', architecture: 'arm64', libc: 'glibc' }; + } + throw adapterError( + 'unsupported_platform', + 'light-ocr currently supports Linux arm64 with glibc only', + key, + ); + } const identity = identities[key]; if (!identity) { throw adapterError('unsupported_platform', `light-ocr does not support ${key}`, key); @@ -41,8 +53,10 @@ function platformPackage() { const packages = { 'macos-arm64': '@arcships/light-ocr-darwin-arm64', 'macos-x64': '@arcships/light-ocr-darwin-x64', + 'windows-arm64': '@arcships/light-ocr-win32-arm64', 'windows-x64': '@arcships/light-ocr-win32-x64', 'linux-x64': '@arcships/light-ocr-linux-x64-gnu', + 'linux-arm64': '@arcships/light-ocr-linux-arm64-gnu', }; return packages[platformIdentity().id]; } diff --git a/bindings/node/package.json b/bindings/node/package.json index d35abe1..3d9b5d6 100644 --- a/bindings/node/package.json +++ b/bindings/node/package.json @@ -1,6 +1,6 @@ { "name": "@arcships/light-ocr", - "version": "0.3.0", + "version": "0.3.1", "private": true, "description": "Node-API adapter for the light-ocr C++ core", "license": "Apache-2.0", diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 0c8cde4..930482c 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -185,7 +185,11 @@ function(light_ocr_configure_dependencies) else() set(_ort_include "${onnxruntime_package_SOURCE_DIR}/build/native/include") if(WIN32) - set(_ort_runtime_dir "runtimes/win-x64/native") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") + set(_ort_runtime_dir "runtimes/win-arm64/native") + else() + set(_ort_runtime_dir "runtimes/win-x64/native") + endif() set(_ort_library "${onnxruntime_package_SOURCE_DIR}/${_ort_runtime_dir}/onnxruntime.dll") set(_ort_implib "${onnxruntime_package_SOURCE_DIR}/${_ort_runtime_dir}/onnxruntime.lib") elseif(APPLE) @@ -210,6 +214,15 @@ function(light_ocr_configure_dependencies) file(CREATE_LINK "libonnxruntime.so" "${_ort_soname_library}" SYMBOLIC) endif() set(_ort_runtime_files "${_ort_library}" "${_ort_soname_library}") + elseif(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + set(_ort_runtime_dir "runtimes/linux-arm64/native") + set(_ort_library "${onnxruntime_package_SOURCE_DIR}/${_ort_runtime_dir}/libonnxruntime.so") + set(_ort_soname_library + "${onnxruntime_package_SOURCE_DIR}/${_ort_runtime_dir}/libonnxruntime.so.1") + if(NOT EXISTS "${_ort_soname_library}") + file(CREATE_LINK "libonnxruntime.so" "${_ort_soname_library}" SYMBOLIC) + endif() + set(_ort_runtime_files "${_ort_library}" "${_ort_soname_library}") else() message(FATAL_ERROR "Unsupported ONNX Runtime target: ${CMAKE_SYSTEM_NAME}/${CMAKE_SYSTEM_PROCESSOR}") diff --git a/docs/build-and-release.md b/docs/build-and-release.md index 9807099..d3ff8c1 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -213,18 +213,18 @@ macOS arm64 高分辨率绝对 RSS gates 由 `light_ocr_memory_gate` 独立进 `.github/workflows/core.yml` 定义三类 job: -- `tier1`:四个 Tier 1 原生 runner,锁定依赖/模型、离线缓存复核、Release 构建、真实模型测试、sterile/offline 检查、RSS gate、manifest/license/SBOM。 +- `tier1`:六个 Tier 1 原生 runner,锁定依赖/模型、离线缓存复核、Release 构建、真实模型测试、sterile/offline 检查、RSS gate、manifest/license/SBOM。 - `safety`:Linux ASan+UBSan+LSan、TSan、四个 libFuzzer 入口。 - `oracle`:hash-locked Python 环境、committed corpus/golden 身份校验、同机 live oracle 的 14 个语料全阶段对齐和首 bundle 质量基线。 `.github/workflows/npm-release.yml` 是仅允许从 `main` 手动触发的发布候选与发布流程。默认 `publish_to_registry=false`,所以第一次运行不会读取 `NPM_TOKEN` 或改动 npm registry: - 0.3.0 候选先在 Linux/Python 3.11 的哈希锁工具链中重现并校验内部 WebGPU FP16 派生工件,再在 macOS/Python 3.12 中派生固定 Core ML FP16 package hashes,最后把合并的 native superset bundle 交给 Linux assemble;WebGPU 公共执行 profile 只发布 FP32,用户安装、postinstall 和首次运行都不会执行转换或联网。 -- 四个平台分别原生构建 Node-API addon,并保存许可证与 SPDX SBOM。 -- 汇聚为一个 facade、一个 model 和四个 native packages,执行两次 `npm pack` 并要求 tarball SHA-256 完全一致。 -- 在 macOS arm64/x64、Linux x64 glibc、Windows x64 上分别使用 Node.js 22 和 24,从本地 tarballs 执行 `--ignore-scripts` 安装、CJS/ESM bounded OCR、单次 tiled contract/结果 smoke 与 TypeScript compile test。 -- 六个 tarball 先发布到一次性 Verdaccio registry,只安装 facade 后停止 registry,再执行真实 bounded 与 tiled OCR,证明没有运行时下载依赖。 -- 只有以上功能/制品 gates、需要时已经单独完成的受审 baseline,以及 `publish_to_registry=true` 同时满足时,`npm-release` GitHub environment 才能读取 `NPM_TOKEN`;先发布五个依赖到 `next`,通过 registry facade 安装后再发布主包,最终禁网运行并可显式提升到 `latest`。 +- 六个平台分别原生构建 Node-API addon,并保存许可证与 SPDX SBOM。 +- 汇聚为一个 facade、一个 model 和六个 native packages,执行两次 `npm pack` 并要求 tarball SHA-256 完全一致。 +- 在 macOS arm64/x64、Linux x64 glibc、Linux arm64 glibc、Windows x64、Windows arm64 上分别使用 Node.js 22 和 24,从本地 tarballs 执行 `--ignore-scripts` 安装、CJS/ESM bounded OCR、单次 tiled contract/结果 smoke 与 TypeScript compile test。 +- 八个 tarball 先发布到一次性 Verdaccio registry,只安装 facade 后停止 registry,再执行真实 bounded 与 tiled OCR,证明没有运行时下载依赖。 +- 只有以上功能/制品 gates、需要时已经单独完成的受审 baseline,以及 `publish_to_registry=true` 同时满足时,`npm-release` GitHub environment 才能读取 `NPM_TOKEN`;先发布七个依赖到 `next`,通过 registry facade 安装后再发布主包,最终禁网运行并可显式提升到 `latest`。 `0.3.0` 发布前 dry-run 的触发命令为: @@ -277,16 +277,16 @@ bytes: 31334400 sha256: 74e246bf075c141da51e58515c731298fdabee9fd5bd8feb7cf6c7f4f352de17 ``` -npm release 按 [npm-packaging.md](npm-packaging.md) 生成一个 facade、一个 model 和四个 native staging packages。model package 保存上述归档的精确解包内容;发布候选另外记录六个 npm tarballs 的 bytes、SHA-256、npm integrity 和 registry identity,不能把 Core USTAR hash 当作 npm tarball hash。 +npm release 按 [npm-packaging.md](npm-packaging.md) 生成一个 facade、一个 model 和六个 native staging packages。model package 保存上述归档的精确解包内容;发布候选另外记录八个 npm tarballs 的 bytes、SHA-256、npm integrity 和 registry identity,不能把 Core USTAR hash 当作 npm tarball hash。 ## 10. 发布候选门槛 1. 在带 revision 的干净 Git snapshot 上运行全部 CI。 -2. 四个 Tier 1 原生 job 全绿;不得用交叉编译替代。 +2. 六个 Tier 1 原生 job 全绿;不得用交叉编译替代。 3. 保存 parity、quality、benchmark、leak、Sanitizer、fuzz 和 offline 报告。 4. 将精确 bundle 文件打入 `@arcships/light-ocr-model-ppocrv6-small`,验证 sterile install,并记录 npm tarball SHA-256/integrity。独立 USTAR mirror 是非 npm 分发项,不阻塞 npm package release。 5. 为每个平台生成 manifest、许可证清单和 SBOM。 -6. 在隔离环境验证六个 npm tarballs、platform 选择、默认 `createEngine()` 和模型 payload hash;已安装后的运行测试必须禁网。 +6. 在隔离环境验证八个 npm tarballs、platform 选择、默认 `createEngine()` 和模型 payload hash;已安装后的运行测试必须禁网。 7. 对照 [implementation-status.md](implementation-status.md) 关闭所有 Pending 项。 registry 发布由受保护的 `npm-release` environment 执行;workflow 成功记录、registry metadata 与安装复验才构成完成证据。签名、公证、非 npm 公共下载地址和长期保留策略仍是独立的外部事项。 diff --git a/docs/napi-design.md b/docs/napi-design.md index e69b6f9..52962be 100644 --- a/docs/napi-design.md +++ b/docs/napi-design.md @@ -34,7 +34,7 @@ Decision:[decisions.md](decisions.md) D101、D105、D111 3. 明确定义输入快照、队列背压、关闭、GC、`worker_threads` 和环境退出行为。 4. 与 C++ Core 做字段级、错误级和结果级 parity。 5. 复用 Core 的模型验证、资源限制和 OCR 算法,不在适配器复制业务逻辑。 -6. 为四个 Core Tier 1 目标提供 Node-API 预编译包。 +6. 为六个 Core Tier 1 目标提供 Node-API 预编译包。 7. `npm install @arcships/light-ocr` 同时取得默认模型,正常调用 `createEngine()` 不需要 `bundlePath`。 ### 2.2 v1 非目标 @@ -641,22 +641,24 @@ bindings/node/ @arcships/light-ocr-darwin-arm64 addon + ONNX Runtime dylib + licenses @arcships/light-ocr-darwin-x64 addon + ONNX Runtime dylib + licenses @arcships/light-ocr-win32-x64 addon + onnxruntime.dll + licenses +@arcships/light-ocr-win32-arm64 addon + onnxruntime.dll + licenses @arcships/light-ocr-linux-x64-gnu addon + ONNX Runtime so + licenses +@arcships/light-ocr-linux-arm64-gnu addon + ONNX Runtime so + licenses ``` -Facade 同时提供 ESM 和 CommonJS exports,但两者加载同一个 environment-aware `.node` addon。native 子路径不作为 public export。model package 是 exact-version 普通 dependency;四个平台包是 exact-version optional dependencies,由 `os`、`cpu` 和 Linux `libc` metadata 筛选。 +Facade 同时提供 ESM 和 CommonJS exports,但两者加载同一个 environment-aware `.node` addon。native 子路径不作为 public export。model package 是 exact-version 普通 dependency;六个平台包是 exact-version optional dependencies,由 `os`、`cpu` 和 Linux `libc` metadata 筛选。 安装规则: - 正常安装不运行 install/postinstall 脚本,不在用户机器隐式编译。模型只作为 npm package payload 由 npm 在安装阶段取得。 - model package 直接包含可读取的 bundle 目录;JS facade 解析绝对路径,native loader 不解压、不联网。 - 平台包包含 `.node`、锁定的 ONNX Runtime 动态库、third-party licenses、SBOM 和 artifact hash。 -- macOS 保持最低 13.3;Windows 使用 MSVC 2022 x64;Linux x64 GNU 基线在首次 N-API release 前用专用构建容器固定,不能把 Ubuntu 24.04 runner 偶然产生的 glibc 要求当作长期 SDK 承诺。 +- macOS 保持最低 13.3;Windows 使用 MSVC 2022(x64 与 arm64);Linux x64/arm64 GNU 基线在首次 N-API release 前用专用构建容器固定,不能把 Ubuntu 24.04 runner 偶然产生的 glibc 要求当作长期 SDK 承诺。 - macOS/Linux 使用相对 loader path,Windows DLL 与 `.node` 同目录。 - source build 是显式开发命令,不是 install fallback。 - 使用 `--omit=optional` 会缺少 native package;facade 必须返回可操作的 `package_load_failed`,不能尝试下载或编译。 -Node-API 解决 Node/V8 ABI 兼容,不消除 OS、architecture、libc、C++ runtime 和 ONNX Runtime 的平台差异。因此仍需四个平台的原生构建和加载测试。 +Node-API 解决 Node/V8 ABI 兼容,不消除 OS、architecture、libc、C++ runtime 和 ONNX Runtime 的平台差异。因此仍需六个平台的原生构建和加载测试。 ## 13. 构建边界 @@ -715,7 +717,7 @@ Node-API 解决 Node/V8 ABI 兼容,不消除 OS、architecture、libc、C++ ru ### 14.6 npm package contract -- 从六个本地 `.tgz` 在 sterile 临时目录安装;CJS、ESM 和 types 都只能使用 package 内容,不能回读仓库。 +- 从八个本地 `.tgz` 在 sterile 临时目录安装;CJS、ESM 和 types 都只能使用 package 内容,不能回读仓库。 - `createEngine()`、`createEngine({})` 和 `model: "ppocrv6-small"` 都加载同一 bundle ID;显式 `bundlePath` 仍工作;`model` 与 `bundlePath` 同时提供时拒绝。 - model package 缺失、bundle ID 不匹配、支持平台 package 缺失、unsupported platform 和 `--omit=optional` 分别得到稳定、可操作的错误。 - `--ignore-scripts` 安装正常;已安装后禁网运行正常;没有 postinstall、下载、解压或源码编译副作用。 @@ -731,10 +733,10 @@ Node-API 解决 Node/V8 ABI 兼容,不消除 OS、architecture、libc、C++ ru 4. **完成**:实现 AbortSignal facade、private cancel、结果/错误的完整字段转换与真实 PP-OCRv6 golden test。 5. **完成**:实现 explicit close、GC finalizer、dispatcher ref/unref 和 teardown-safe environment cleanup。 6. **部分完成**:Node.js 22/macOS arm64 已覆盖 event-loop heartbeat、正常退出和未 close 的 worker environment teardown;worker termination、compatible-host sanitizer、leak 和多平台性能矩阵仍待补。 -7. 固定 Linux GNU baseline,构建四个平台 prebuild,生成 licenses/SBOM/hashes。 -8. 生成 model、四个平台 native 和 facade 六个 package,执行 sterile tarball install test;先发布五个依赖,最后发布 `@arcships/light-ocr` facade。 +7. 固定 Linux GNU baseline,构建六个平台 prebuild,生成 licenses/SBOM/hashes。 +8. 生成 model、六个平台 native 和 facade 八个 package,执行 sterile tarball install test;先发布七个依赖,最后发布 `@arcships/light-ocr` facade。 -前五步完成前不能把 addon 称为可用;四平台矩阵、发布元数据和 prebuild 完成前不能称为 npm release ready。 +前五步完成前不能把 addon 称为可用;六平台矩阵、发布元数据和 prebuild 完成前不能称为 npm release ready。 ## 16. 明确延期项 diff --git a/docs/npm-packaging.md b/docs/npm-packaging.md index cf5a48c..4ae00e8 100644 --- a/docs/npm-packaging.md +++ b/docs/npm-packaging.md @@ -43,7 +43,7 @@ const engine = await createEngine(); ## 2. 包集合与依赖图 -首个 release set 固定为六个公开 scoped packages: +首个 release set 固定为八个公开 scoped packages: | 包 | 类型 | 内容 | 安装关系 | | --- | --- | --- | --- | @@ -52,7 +52,9 @@ const engine = await createEngine(); | `@arcships/light-ocr-darwin-arm64` | native | arm64 `.node`、ONNX Runtime dylib、licenses、SBOM、hashes | facade 的 optional dependency | | `@arcships/light-ocr-darwin-x64` | native | x64 `.node`、ONNX Runtime dylib、licenses、SBOM、hashes | facade 的 optional dependency | | `@arcships/light-ocr-win32-x64` | native | x64 `.node`、`onnxruntime.dll`、licenses、SBOM、hashes | facade 的 optional dependency | +| `@arcships/light-ocr-win32-arm64` | native | arm64 `.node`、`onnxruntime.dll`、licenses、SBOM、hashes | facade 的 optional dependency | | `@arcships/light-ocr-linux-x64-gnu` | native | glibc x64 `.node`、ONNX Runtime `.so`、licenses、SBOM、hashes | facade 的 optional dependency | +| `@arcships/light-ocr-linux-arm64-gnu` | native | glibc arm64 `.node`、ONNX Runtime `.so`、licenses、SBOM、hashes | facade 的 optional dependency | 依赖方向只有两层: @@ -63,7 +65,9 @@ const engine = await createEngine(); ├── @arcships/light-ocr-darwin-arm64 ├── @arcships/light-ocr-darwin-x64 ├── @arcships/light-ocr-win32-x64 - └── @arcships/light-ocr-linux-x64-gnu + ├── @arcships/light-ocr-win32-arm64 + ├── @arcships/light-ocr-linux-x64-gnu + └── @arcships/light-ocr-linux-arm64-gnu ``` 模型包不能是 optional dependency。否则 `--omit=optional`、平台筛选或安装器的可选依赖容错会让一次“成功安装”缺少默认模型,违背主包契约。native 包必须是 optional dependencies,因为同一机器只需要一个平台产物。 @@ -99,7 +103,9 @@ v1 不提供无模型的 `core`/`lite` 入口,也不允许用户单独拼装 f "@arcships/light-ocr-darwin-arm64": "0.3.0", "@arcships/light-ocr-darwin-x64": "0.3.0", "@arcships/light-ocr-linux-x64-gnu": "0.3.0", - "@arcships/light-ocr-win32-x64": "0.3.0" + "@arcships/light-ocr-linux-arm64-gnu": "0.3.0", + "@arcships/light-ocr-win32-x64": "0.3.0", + "@arcships/light-ocr-win32-arm64": "0.3.0" }, "publishConfig": { "access": "public" } } @@ -169,7 +175,7 @@ Facade 在创建 engine 前读取 manifest,并核对 `bundleId` 是否等于 } ``` -macOS 包使用 `os: ["darwin"]` 和对应 `cpu`;Windows 包使用 `os: ["win32"]`、`cpu: ["x64"]`。package root 只导出 `native/light_ocr_node.node`,facade 的共享 CommonJS loader 因而可直接 `require(packageName)`。macOS/Linux 动态库使用相对 loader path,Windows DLL 与 `.node` 放在固定相邻目录。package 不得依赖消费者预装 ONNX Runtime、OpenCV 或编译工具链。 +macOS 包使用 `os: ["darwin"]` 和对应 `cpu`;Windows 包使用 `os: ["win32"]`、`cpu` 为 `["x64"]` 或 `["arm64"]`。package root 只导出 `native/light_ocr_node.node`,facade 的共享 CommonJS loader 因而可直接 `require(packageName)`。macOS/Linux 动态库使用相对 loader path,Windows DLL 与 `.node` 放在固定相邻目录。package 不得依赖消费者预装 ONNX Runtime、OpenCV 或编译工具链。 Facade 只按固定映射加载 package: @@ -178,7 +184,9 @@ Facade 只按固定映射加载 package: | `darwin` | `arm64` | `@arcships/light-ocr-darwin-arm64` | | `darwin` | `x64` | `@arcships/light-ocr-darwin-x64` | | `win32` | `x64` | `@arcships/light-ocr-win32-x64` | +| `win32` | `arm64` | `@arcships/light-ocr-win32-arm64` | | `linux` + glibc | `x64` | `@arcships/light-ocr-linux-x64-gnu` | +| `linux` + glibc | `arm64` | `@arcships/light-ocr-linux-arm64-gnu` | 未知组合以 `unsupported_platform` 拒绝 `createEngine()`。已支持组合但 native package 缺失时,以 `package_load_failed` 拒绝,并提示重新安装且不要使用 `--omit=optional`;不能静默源码编译或在线下载二进制。开发环境仍可显式设置 `LIGHT_OCR_NODE_BINARY`,但 published README 不把它当作生产配置。 @@ -227,7 +235,7 @@ export function createEngine(options?: CreateEngineOptions): Promise; ## 7. 版本策略 -六个 package 使用 lockstep SemVer:同一次 release set 的版本完全相同。Facade 对 model/native 始终使用 exact version,因此不能加载另一批次的 Core、ORT 或模型。 +八个 package 使用 lockstep SemVer:同一次 release set 的版本完全相同。Facade 对 model/native 始终使用 exact version,因此不能加载另一批次的 Core、ORT 或模型。 模型内容身份与 npm 版本分离: @@ -247,18 +255,18 @@ bindings/node/js + facade manifest template models/generated/ppocrv6-small-onnx-20260714.2 reports/release/ native artifacts ↓ -dist/npm/ +dist/npm/ ``` `dist/npm` 是临时生成目录,不是源码 authority。打包器必须使用 `files` allowlist,并拒绝 source、test fixture、cache、绝对路径、symlink、额外动态库和未登记文件。 发布顺序必须是原子可恢复的: -1. 生成六个 staging directories,验证 package metadata 和文件 inventory。 +1. 生成八个 staging directories,验证 package metadata 和文件 inventory。 2. 对每个目录执行 `npm pack --dry-run`,再生成 `.tgz` 并记录 filename、bytes、SHA-256 和 npm integrity。 -3. 把六个 `.tgz` 放入一次性本地 npm registry;在无仓库文件可见的临时目录只安装 facade,验证 exact dependency graph、平台筛选和真实模型。 -4. 先发布 model package 和四个 native packages 到 `next`。 -5. 确认五个依赖都可读取且 metadata 正确后,最后发布 facade 到 `next`。 +3. 把八个 `.tgz` 放入一次性本地 npm registry;在无仓库文件可见的临时目录只安装 facade,验证 exact dependency graph、平台筛选和真实模型。 +4. 先发布 model package 和六个 native packages 到 `next`。 +5. 确认七个依赖都可读取且 metadata 正确后,最后发布 facade 到 `next`。 6. 完整 release evidence 归档后再移动 dist-tag。 `@arcships` scope 必须已由发布账号或组织控制。scoped package 的发布清单固定 `publishConfig.access: public`,发布流程也显式使用 public access,避免 scope 默认私有策略造成误配置。 @@ -275,7 +283,7 @@ dist/npm/ 一个 npm release candidate 至少满足: -- 四个 native packages 分别在目标 OS/arch 原生构建;Node.js 22/24 加载、真实 OCR、AbortSignal、close、GC 和 worker teardown 全绿。 +- 六个 native packages 分别在目标 OS/arch 原生构建;Node.js 22/24 加载、真实 OCR、AbortSignal、close、GC 和 worker teardown 全绿。 - `npm pack --dry-run` inventory 与 allowlist 完全一致;package 内没有源码、缓存、测试图像、原始上游 archive 或绝对构建路径。 - Facade 的 ESM、CJS 和 TypeScript compile tests 均通过。 - 从一次性本地 registry 执行 `npm install @arcships/light-ocr` 后,`createEngine()` 不传 `bundlePath` 即完成真实 PP-OCRv6 识别。 @@ -283,15 +291,15 @@ dist/npm/ - 在网络禁用环境里,对已经安装好的 package 重复 create/recognize/close 成功。 - 模型 package 的 bundle 文件总字节、manifest、`SHA256SUMS` 和 bundle ID 与 `models/bundles.lock.json` 对应生成物一致。 - native package 的 addon、ORT library、artifact hashes、license inventory 和 SPDX SBOM 一致。 -- 从干净 release commit 生成并记录六个 npm tarballs 的 SHA-256、registry integrity、dist-tag 和 CI artifact URL。 +- 从干净 release commit 生成并记录八个 npm tarballs 的 SHA-256、registry integrity、dist-tag 和 CI artifact URL。 - 仓库根 `LICENSE`/`NOTICE`、facade/native package 的 SPDX `license` 字段与 Apache-2.0 一致。 ## 10. v1 明确不做 - install/postinstall 时从 GitHub、对象存储或 Paddle 官方地址下载模型或二进制。 - 首次 `createEngine()` 时下载、解压或自动更新模型。 -- 将模型直接复制进四个 native packages;这会造成四份重复分发。 +- 将模型直接复制进六个 native packages;这会造成六份重复分发。 - 无模型 facade、按需语言包、tiny/medium、orientation 或 GPU packages。 - 源码编译 fallback、`node-gyp` fallback 或消费者系统 ORT fallback。 -- Electron、Bun、Deno、Linux musl/arm64、Windows arm64 支持声明。 +- Electron、Bun、Deno、Linux musl 支持声明。 - Yarn Plug'n'Play/zip archive 兼容声明;v1 release gate 以官方 npm 的物理安装目录为准。 diff --git a/models/deps.lock.json b/models/deps.lock.json index 244b480..24c66cc 100644 --- a/models/deps.lock.json +++ b/models/deps.lock.json @@ -17,9 +17,11 @@ "runtimeFlavor": "cpu", "platforms": [ "linux-x64-gnu", + "linux-arm64-gnu", "macos-arm64", "macos-x64", - "windows-x64" + "windows-x64", + "windows-arm64" ] }, { diff --git a/tests/python/npm_release_fixtures.py b/tests/python/npm_release_fixtures.py index 9520ff6..1038874 100644 --- a/tests/python/npm_release_fixtures.py +++ b/tests/python/npm_release_fixtures.py @@ -107,6 +107,10 @@ def current_platform_id() -> str | None: return "macos-x64" if system == "Linux" and machine in {"x86_64", "amd64"}: return "linux-x64" + if system == "Linux" and machine in {"arm64", "aarch64"}: + return "linux-arm64" if system == "Windows" and machine in {"x86_64", "amd64"}: return "windows-x64" + if system == "Windows" and machine in {"arm64", "aarch64"}: + return "windows-arm64" return None diff --git a/tests/python/test_npm_release.py b/tests/python/test_npm_release.py index 089afe1..ced1cdf 100644 --- a/tests/python/test_npm_release.py +++ b/tests/python/test_npm_release.py @@ -90,7 +90,7 @@ def test_dist_tag_verification_waits_for_registry_convergence( self.assertEqual(dist_tag.call_count, 2) sleep.assert_called_once_with(3) - def test_stages_and_deterministically_packs_six_packages(self) -> None: + def test_stages_and_deterministically_packs_all_release_packages(self) -> None: npm = shutil.which("npm") if npm is None: self.skipTest("npm is unavailable") @@ -115,9 +115,12 @@ def test_stages_and_deterministically_packs_six_packages(self) -> None: ) staging = root / "staging" + source_version = npm_release.read_json( + npm_release.ROOT / "bindings" / "node" / "package.json" + )["version"] npm_release.assemble( argparse.Namespace( - version="0.3.0", + version=source_version, bundle=model_bundle(root), native_root=native_root, output_dir=staging, @@ -126,8 +129,10 @@ def test_stages_and_deterministically_packs_six_packages(self) -> None: facade = json.loads( (staging / "facade" / "package.json").read_text("utf-8") ) - self.assertEqual(facade["dependencies"][npm_release.MODEL_PACKAGE], "0.3.0") - self.assertEqual(len(facade["optionalDependencies"]), 4) + self.assertEqual(facade["dependencies"][npm_release.MODEL_PACKAGE], source_version) + self.assertEqual( + len(facade["optionalDependencies"]), len(npm_release.PLATFORMS) + ) model = json.loads( (staging / "model-ppocrv6-small" / "package.json").read_text("utf-8") ) @@ -142,9 +147,12 @@ def test_stages_and_deterministically_packs_six_packages(self) -> None: release = json.loads( (tarballs / "release-manifest.json").read_text("utf-8") ) - self.assertEqual(release["version"], "0.3.0") - self.assertEqual(len(release["packages"]), 6) - self.assertEqual(len(list(tarballs.glob("*.tgz"))), 6) + self.assertEqual(release["version"], source_version) + expected_packages = len(npm_release.PLATFORMS) + 2 + self.assertEqual(len(release["packages"]), expected_packages) + self.assertEqual( + len(list(tarballs.glob("*.tgz"))), expected_packages + ) platform_id = current_platform_id() if platform_id is None: diff --git a/tools/npm_release.py b/tools/npm_release.py index 91c6591..0f203c0 100644 --- a/tools/npm_release.py +++ b/tools/npm_release.py @@ -54,6 +54,14 @@ "libc": ["glibc"], "runtime": "libonnxruntime.so.1", }, + "linux-arm64": { + "package": "@arcships/light-ocr-linux-arm64-gnu", + "os": ["linux"], + "cpu": ["arm64"], + "architecture": "arm64", + "libc": ["glibc"], + "runtime": "libonnxruntime.so.1", + }, "windows-x64": { "package": "@arcships/light-ocr-win32-x64", "os": ["win32"], @@ -61,6 +69,13 @@ "architecture": "x86_64", "runtime": "onnxruntime.dll", }, + "windows-arm64": { + "package": "@arcships/light-ocr-win32-arm64", + "os": ["win32"], + "cpu": ["arm64"], + "architecture": "arm64", + "runtime": "onnxruntime.dll", + }, } @@ -959,10 +974,13 @@ def package_directories(staging: Path) -> list[Path]: packages = sorted( path for path in staging.iterdir() if (path / "package.json").is_file() ) - if len(packages) != 6: - raise RuntimeError(f"expected six staged packages, found {len(packages)}") + expected = len(PLATFORMS) + 2 + if len(packages) != expected: + raise RuntimeError( + f"expected {expected} staged packages, found {len(packages)}" + ) names = [read_json(path / "package.json")["name"] for path in packages] - if len(set(names)) != 6: + if len(set(names)) != expected: raise RuntimeError("staged package names are not unique") return packages