diff --git a/.gitattributes b/.gitattributes index 42f4ed8..b26e72a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ -# Native source fingerprints must match across checkout platforms. +# Line endings stay LF on every checkout, Windows included, so every host +# builds from the same bytes. /rust/** text eol=lf /hook/build.dart text eol=lf /lib/src/epic_cash_wallet_bindings.g.dart text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index d064003..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -permissions: - contents: read - -env: - CARGO_TERM_COLOR: always - -jobs: - host-native-tests: - strategy: - fail-fast: false - matrix: - os: - - ubuntu-24.04 - - macos-latest - - windows-latest - runs-on: ${{ matrix.os }} - timeout-minutes: 45 - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.99.1" - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: host-${{ matrix.os }} - - - uses: actions/cache@v5 - with: - path: .dart_tool/hooks_runner - key: native-assets-${{ matrix.os }}-${{ hashFiles('rust/Cargo.lock', 'rust/**/*.rs', 'hook/build.dart') }} - - - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.47.2 - channel: stable - cache: true - - - name: Install Linux native prerequisites - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y clang cmake libclang-dev ninja-build pkg-config - - - name: Shorten Windows build path - if: runner.os == 'Windows' - run: | - subst T: "$env:GITHUB_WORKSPACE" - echo 'BUILD_DIR=T:\' >> $env:GITHUB_ENV - - - run: flutter pub get - working-directory: ${{ env.BUILD_DIR || github.workspace }} - - run: flutter test - working-directory: ${{ env.BUILD_DIR || github.workspace }} - - - name: Verify focused analysis and generated bindings - if: runner.os == 'Linux' - run: | - dart analyze hook tool test/build_hook_test.dart lib/epic_cash.dart lib/flutter_libepiccash.dart - dart run ffigen --config ffigen.yaml - git diff --exit-code -- lib/src/epic_cash_wallet_bindings.g.dart - - apple-example-builds: - runs-on: macos-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.99.1" - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: apple-examples - - - uses: actions/cache@v5 - with: - path: example/.dart_tool/hooks_runner - key: native-assets-apple-examples-${{ hashFiles('rust/Cargo.lock', 'rust/**/*.rs', 'hook/build.dart') }} - - - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.47.2 - channel: stable - cache: true - - - run: flutter pub get - - run: flutter pub get - working-directory: example - - run: flutter test - working-directory: example - - run: flutter build macos --debug - working-directory: example - - run: flutter build ios --simulator --debug --no-codesign - working-directory: example - - rust-tests: - runs-on: ubuntu-24.04 - timeout-minutes: 60 - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.99.1" - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: rust-tests - - - name: Install Linux native prerequisites - run: | - sudo apt-get update - sudo apt-get install -y clang cmake libclang-dev ninja-build pkg-config - - - name: Run Rust unit tests - run: cargo test --locked --lib - working-directory: rust diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml new file mode 100644 index 0000000..6752bb6 --- /dev/null +++ b/.github/workflows/native.yml @@ -0,0 +1,25 @@ +# Builds the Rust library on every reference host, fails if any host or the +# committed dnix.lock disagrees, and publishes the release the lock names +# when it reaches the default branch. +name: Native library + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: write + +# A newer push supersedes runs on a branch, never on main: a run there may +# be publishing. +concurrency: + group: native-${{ github.ref }} + cancel-in-progress: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }} + +jobs: + native: + # The same dnix commit as pubspec.yaml and flake.lock. + uses: cypherstack/dnix/.github/workflows/native.yml@e91763ef25d084b3a5cf271a0cfee69159b1f779 + with: + macos-runner: xcode-27 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index aa34bbb..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,292 +0,0 @@ -name: Build native artifacts - -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "native-*" - workflow_dispatch: - -permissions: - contents: read - -env: - CARGO_TERM_COLOR: always - -concurrency: - group: native-prebuilts-${{ github.ref }} - cancel-in-progress: false - -jobs: - build-linux: - runs-on: ubuntu-24.04 - container: - image: ghcr.io/cypherstack/stackwallet-ci:latest - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - asset: libepic_cash_wallet-x86_64-unknown-linux-gnu.so - - target: aarch64-unknown-linux-gnu - asset: libepic_cash_wallet-aarch64-unknown-linux-gnu.so - steps: - - uses: actions/checkout@v6 - - - name: Select Rust toolchain - run: | - rustup toolchain install 1.99.1 --profile minimal --target "${{ matrix.target }}" - rustup default 1.99.1 - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: linux-${{ matrix.target }} - - - name: Build dynamic library - env: - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++ - run: | - cd rust - cargo build --locked --release --lib --target "${{ matrix.target }}" - cp "target/${{ matrix.target }}/release/libepic_cash_wallet.so" "${{ matrix.asset }}" - - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.asset }} - path: rust/${{ matrix.asset }} - if-no-files-found: error - - build-android: - runs-on: ubuntu-24.04 - container: - image: ghcr.io/cypherstack/stackwallet-ci:latest - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-linux-android - abi: arm64-v8a - asset: libepic_cash_wallet-aarch64-linux-android.so - - target: armv7-linux-androideabi - abi: armeabi-v7a - asset: libepic_cash_wallet-armv7-linux-androideabi.so - - target: x86_64-linux-android - abi: x86_64 - asset: libepic_cash_wallet-x86_64-linux-android.so - steps: - - uses: actions/checkout@v6 - - - name: Select Rust toolchain - run: | - rustup toolchain install 1.99.1 --profile minimal --target "${{ matrix.target }}" - rustup default 1.99.1 - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: android-${{ matrix.target }} - - - name: Build dynamic library - run: | - cd rust - cargo ndk --target "${{ matrix.target }}" --platform 24 \ - --link-libcxx-shared --output-dir native-libs \ - build --locked --release --lib - cp "target/${{ matrix.target }}/release/libepic_cash_wallet.so" "${{ matrix.asset }}" - cp "native-libs/${{ matrix.abi }}/libc++_shared.so" "libc++_shared-${{ matrix.target }}.so" - - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.asset }} - path: | - rust/${{ matrix.asset }} - rust/libc++_shared-${{ matrix.target }}.so - if-no-files-found: error - - build-ios: - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-apple-ios - sdk: iphoneos - asset: libepic_cash_wallet-aarch64-apple-ios.dylib - - target: aarch64-apple-ios-sim - sdk: iphonesimulator - asset: libepic_cash_wallet-aarch64-apple-ios-sim.dylib - - target: x86_64-apple-ios - sdk: iphonesimulator - asset: libepic_cash_wallet-x86_64-apple-ios.dylib - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.99.1" - targets: ${{ matrix.target }} - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: ios-${{ matrix.target }} - - - name: Build dynamic library - env: - IPHONEOS_DEPLOYMENT_TARGET: "15.0" - run: | - export SDKROOT="$(xcrun --sdk "${{ matrix.sdk }}" --show-sdk-path)" - cd rust - cargo build --locked --release --lib --target "${{ matrix.target }}" - cp "target/${{ matrix.target }}/release/libepic_cash_wallet.dylib" "${{ matrix.asset }}" - - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.asset }} - path: rust/${{ matrix.asset }} - if-no-files-found: error - - build-macos: - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - target: - - aarch64-apple-darwin - - x86_64-apple-darwin - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.99.1" - targets: ${{ matrix.target }} - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: macos-${{ matrix.target }} - - - name: Build dynamic library - env: - MACOSX_DEPLOYMENT_TARGET: "12.0" - run: | - cd rust - cargo build --locked --release --lib --target "${{ matrix.target }}" - cp "target/${{ matrix.target }}/release/libepic_cash_wallet.dylib" \ - "libepic_cash_wallet-${{ matrix.target }}.dylib" - - - uses: actions/upload-artifact@v4 - with: - name: libepic_cash_wallet-${{ matrix.target }}.dylib - path: rust/libepic_cash_wallet-${{ matrix.target }}.dylib - if-no-files-found: error - - build-windows: - runs-on: windows-latest - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.99.1" - targets: x86_64-pc-windows-msvc - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust - key: windows-x86_64-pc-windows-msvc - - - name: Check native prerequisites - shell: pwsh - run: | - cmake --version - perl --version - - - name: Build dynamic library - shell: pwsh - run: | - Set-Location rust - cargo build --locked --release --lib --target x86_64-pc-windows-msvc - Copy-Item ` - target/x86_64-pc-windows-msvc/release/epic_cash_wallet.dll ` - libepic_cash_wallet-x86_64-pc-windows-msvc.dll - - - uses: actions/upload-artifact@v4 - with: - name: libepic_cash_wallet-x86_64-pc-windows-msvc.dll - path: rust/libepic_cash_wallet-x86_64-pc-windows-msvc.dll - if-no-files-found: error - - release: - needs: - - build-linux - - build-android - - build-ios - - build-macos - - build-windows - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - - - uses: actions/download-artifact@v4 - with: - path: artifacts - merge-multiple: true - - - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.47.2 - channel: stable - cache: true - - - run: flutter pub get - - - name: Assemble manifest and checksums - run: | - dart --packages=.dart_tool/package_config.json tool/prebuilt_manifest.dart artifacts - cp rust/include/epic_cash_wallet.h artifacts/ - cd artifacts - find . -maxdepth 1 -type f ! -name checksums.txt -print0 \ - | sort -z | xargs -0 sha256sum > checksums.txt - - - uses: actions/upload-artifact@v4 - with: - name: native-release - path: artifacts/* - if-no-files-found: error - - - name: Publish a new release - if: startsWith(github.ref, 'refs/tags/') - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.ref_name }} - shell: bash - run: | - [[ "$RELEASE_TAG" =~ ^(native-[A-Za-z0-9][A-Za-z0-9._-]*|v[0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo 'Invalid release tag'; exit 1; } - source_commit=$(git rev-parse HEAD) - git fetch --depth=1 --no-tags origin "refs/tags/$RELEASE_TAG" - [[ "$(git rev-parse 'FETCH_HEAD^{commit}')" == "$source_commit" ]] || { echo 'Tag moved during this build'; exit 1; } - read -r manifest_pin _ < artifacts/manifest.sha256 - cat > "$RUNNER_TEMP/native-release-notes.md" <> "$GITHUB_OUTPUT" + - name: Choose the build mode + shell: bash + run: echo '${{ steps.release.outputs.published == 'true' && 'prebuilt' || 'source' }}' > .dnix-build + - name: Run flutter test + shell: bash + run: | + if ! flutter test 2>&1 | tee test.log; then + echo "::error::$(tail -c 1500 test.log | tr '\n' ' ')" + exit 1 + fi + - name: Verify the generated bindings + if: runner.os == 'Linux' && runner.arch == 'X64' + run: | + sudo apt-get update && sudo apt-get install -y libclang-dev + dart run ffigen --config ffigen.yaml + git diff --exit-code -- lib/src/epic_cash_wallet_bindings.g.dart + + # A Windows process can only use the prebuilt library, so this runs once + # the lock's release is published from the default branch, and is the + # one place the Windows DLL is loaded rather than compared by hash. + windows: + name: Windows, prebuilt library + runs-on: windows-2025 + timeout-minutes: 60 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: subosito/flutter-action@v2 + with: + flutter-version: 3.47.2 + channel: stable + cache: true + - name: Check whether the lock's release is published + id: release + run: | + repository=$(jq -r .repository dnix.json | sed 's/\.git$//; s#/$##') + tag="native-$(jq -r .release dnix.lock)" + echo "published=$([ "$(curl -s -o /dev/null -w '%{http_code}' "$repository/releases/tag/$tag")" = 200 ] && echo true || echo false)" >> "$GITHUB_OUTPUT" + - name: Skip until the release exists + if: steps.release.outputs.published != 'true' + run: echo "::notice::No release $(jq -r .release dnix.lock) yet; the Windows tests run once it is published from the default branch." + - name: Run flutter test with the prebuilt library + if: steps.release.outputs.published == 'true' + run: | + flutter pub get + echo prebuilt > .dnix-build + if ! flutter test 2>&1 | tee test.log; then + echo "::error::$(tail -c 1500 test.log | tr '\n' ' ')" + exit 1 + fi + + # The example app builds for macOS and the iOS simulator, as before the + # native library moved to dnix. Source mode needs the pinned Xcode. + apple-example-builds: + name: Example app on macOS and the iOS simulator + runs-on: xcode-27 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: subosito/flutter-action@v2 + with: + flutter-version: 3.47.2 + channel: stable + cache: true + - uses: DeterminateSystems/nix-installer-action@v23 + - run: flutter pub get + - name: Check whether the lock's release is published + id: release + shell: bash + run: | + repository=$(jq -r .repository dnix.json | sed 's/\.git$//; s#/$##') + tag="native-$(jq -r .release dnix.lock)" + echo "published=$([ "$(curl -s -o /dev/null -w '%{http_code}' "$repository/releases/tag/$tag")" = 200 ] && echo true || echo false)" >> "$GITHUB_OUTPUT" + - name: Choose the build mode + shell: bash + run: echo '${{ steps.release.outputs.published == 'true' && 'prebuilt' || 'source' }}' > .dnix-build + - run: flutter test + working-directory: example + - run: flutter build macos --debug + working-directory: example + - run: flutter build ios --simulator --debug --no-codesign + working-directory: example + + rust: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@1.99.1 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + - name: Install Linux native prerequisites + run: | + sudo apt-get update + sudo apt-get install -y clang cmake libclang-dev ninja-build pkg-config + - name: Run Rust unit tests + run: cargo test --locked --lib + working-directory: rust diff --git a/.gitignore b/.gitignore index 1b983d0..087637a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,6 @@ migrate_working_dir/ .dart_tool/ .packages build/ -/patches/ -/prebuilt/ + +# Per-checkout dnix build mode override (see README). +.dnix-build diff --git a/CHANGELOG.md b/CHANGELOG.md index bf04277..c6f4980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ - Pin Rust 1.99.1. Earlier toolchains strip Apple dynamic libraries with a misaligned `__LINKEDIT` string pool, which iOS 27 and macOS 27 refuse to load. -- Add opt-in verified native downloads and release manifests. - Require Flutter 3.47 and Dart 3.13. - Build and bundle the Rust dynamic library with Flutter Native Assets. - Replace runtime `DynamicLibrary` lookup code with generated `@Native` @@ -12,8 +11,19 @@ string. - Remove the legacy Flutter plugin classes, podspecs, CMake targets, copied binaries, and platform build/download scripts. -- Add target-named prebuilt asset support, ABI/smoke tests, and native artifact - release automation. +- Build the native library with [dnix](https://github.com/cypherstack/dnix). + By default the build hook downloads the library of the release `dnix.lock` + names and verifies its SHA-256; the `build` user define selects `source` or + `dev`, which build with Nix on Linux or an Apple Silicon Mac. The + `native_build`, `prebuilt_manifest_url`, and `prebuilt_manifest_sha256` + user defines are no longer read. Every change is built on four hosts, which + must produce the bytes the lock records. +- Build the Windows library for the GNU ABI; it imports only system DLLs. +- Link the C++ runtime statically on Android, so `libc++_shared.so` no longer + ships beside the library. +- Build the iOS libraries for iOS 13 (the arm64 simulator for 14), the + version Flutter 3.47 gives native assets. +- Add ABI and smoke tests. ## 0.0.1 diff --git a/README.md b/README.md index da44863..1586ee6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,83 @@ # flutter_libepiccash Dart bindings for the Epic Cash wallet library. The Rust library is built and -bundled through Flutter Native Assets, using Flutter's standard asset bundling -instead of the former plugin packaging and platform-specific download scripts. +bundled through Flutter Native Assets. ## Requirements -- Flutter 3.47 or newer -- Dart 3.13 or newer -- For source builds: Rust 1.99.1 through `rustup` (selected by - [`rust/rust-toolchain.toml`](rust/rust-toolchain.toml)) -- The normal native build tools for the target platform: Xcode on Apple - platforms, the Android NDK for Android, a C/C++ toolchain and CMake on - Linux, or Visual Studio Build Tools plus Perl on Windows +- Flutter 3.47 or newer, Dart 3.13 or newer. Nothing else: the build hook + downloads the library built for this version and checks its hash. +- To build the library yourself: Nix on Linux or an Apple Silicon Mac, and + Xcode 27.0 for the Apple targets. OpenSSL and RandomX are compiled from + source inside that build. The example projects target Android API 24, iOS 15, and macOS 12 or newer. -## Build from source +## Native library -Source builds are the default. Once Rust and the platform toolchain are -available, use the package like any other Flutter dependency: +The hook obtains the Rust library in one of three modes, chosen with the +`build` user define in the consuming app's root `pubspec.yaml` (the workspace +root for pub workspaces), or for this checkout with a `.dnix-build` file +holding the mode: -```sh -flutter pub get -flutter run +```yaml +hooks: + user_defines: + flutter_libepiccash: + build: prebuilt # the default; or source, or dev ``` -Flutter runs `hook/build.dart`, compiles the Rust crate for the application's -target architecture, and bundles the resulting dynamic library. The first -build can take several minutes because OpenSSL, RandomX, and the wallet -dependencies are compiled from source. Cargo and Flutter cache subsequent -builds. +- `prebuilt` downloads the library of the release `dnix.lock` names and + verifies it against the hash recorded there. A mismatch is an error, never + a fallback. Downloads are cached by hash and need public HTTPS. +- `source` builds the library with Nix, the same build CI uses, and verifies + it against the same hash. +- `dev` builds with Nix but skips the Xcode pin and the hash check, for + working on the Rust code. The hook notices a `.dnix-build` file from its + next run on, so touch `dnix.json` after creating it. + +Only dynamic libraries are provided, for Android API 24, iOS 13 (the arm64 +simulator 14), macOS 12, and glibc 2.28 at minimum, on armv7, arm64, and +x64 Android, arm64 and x64 macOS, iOS devices and simulators, x64 and arm64 +Linux, and x64 Windows. iOS is 13 because Flutter 3.47 asks build hooks +for iOS 13 and writes 13.0 into each native asset's framework as its +minimum; applications can still require a newer iOS. +The Windows library is built for the GNU ABI (`x86_64-pc-windows-gnullvm`, +MinGW-w64 as zig builds it): RandomX's C++ build assumes Microsoft's +compiler and assembler on the MSVC side, which no other host can provide, +while its GCC-style build works unchanged. The DLL links its runtime +pieces statically and imports only system DLLs, so it needs no Visual C++ +redistributable. There is no arm64 Windows library: the dependency tree +still carries `winapi` 0.2, which knows no arm64. The Android libraries +link the C++ runtime statically, so no `libc++_shared.so` ships beside +them. The floors, the Rust and Xcode versions, and the location of the Rust +workspace live in `dnix.json`. + +### Releases + +`dnix.lock` records the hash of every library the Rust source builds to and +names the GitHub release, `native-`, that holds those bytes. After +changing the Rust code, run `dart tool/update_lock.dart` on a Mac with Nix +and Xcode 27.0 and commit the lock with the change; without a Mac, push and +copy the lock CI prints once every host is green. CI +(`.github/workflows/native.yml`) builds on four hosts, fails if any host or +the lock disagrees, and publishes the release when the lock reaches the +default branch. Nothing is committed by CI, and old releases stay. + +Anyone can rebuild one platform with Nix and compare: +`dart tool/verify_release.dart linux` (or android, windows, macos, ios, or +a target triple), on Windows inside WSL2 for the non-Apple platforms. The +build comes from the shared [dnix](https://github.com/cypherstack/dnix) +package, whose documentation explains how the bytes stay identical across +hosts. + +### Tests + +`flutter test` runs the Dart tests against the bundled library, and +`cargo test --locked --lib` in `rust` runs the Rust unit tests. Before the +lock's release is published, or to test local Rust changes, the Dart tests +need the library built from source: `echo dev > .dnix-build` with Nix +installed (`source` to also check the hash). ## Migrate an existing application @@ -40,6 +87,11 @@ Native Assets installation rules in desktop applications; see the example's [`windows/CMakeLists.txt`](example/windows/CMakeLists.txt) and [`linux/CMakeLists.txt`](example/linux/CMakeLists.txt). +The `native_build`, `prebuilt_manifest_url`, and `prebuilt_manifest_sha256` +user defines are no longer read; remove them. The library comes from the +release this package's `dnix.lock` names, so pinning the package's commit +pins the library too, and `build: source` builds it from source with Nix. + Android applications must declare Internet access in `android/app/src/main/AndroidManifest.xml`, directly inside ``, so release builds can connect to the node and Epicbox. The removed plugin manifest @@ -49,54 +101,6 @@ previously supplied this permission: ``` -## Use prebuilt native assets - -To download a release, copy its manifest URL and SHA-256 into the application's -root `pubspec.yaml` (workspace root for pub workspaces): - -```yaml -hooks: - user_defines: - flutter_libepiccash: - native_build: prebuilt - prebuilt_manifest_url: https://github.com/cypherstack/flutter_libepiccash/releases/download/native-YOUR-RELEASE/manifest.json - prebuilt_manifest_sha256: "REPLACE_WITH_RELEASE_MANIFEST_SHA256" -``` - -Use the package revision named in the release. The hook verifies the manifest, -native source fingerprint, deployment minimum, and each library's size and hash, -including Android's C++ runtime. Invalid or missing assets fail the build. -Downloads require public HTTPS and dynamic linking without sanitizers. Rust is -not needed, but Flutter's normal application build tools are still required. - -Downloads live in Flutter's hook output. Each hook run fetches fresh files; -incremental builds can reuse the output until `flutter clean` removes it. -Omit the settings or use `native_build: source` to compile Rust. - -Prebuilts require Android API 24, iOS 15, or macOS 12. Linux deployments must meet -the target's `minimum_glibc_version` in the manifest; musl is unsupported. Windows -releases cover x64. - -## Publish native prebuilts - -Push a new `native-*` tag (for example `native-0.0.1-1`) or `vX.Y.Z` tag. The -release workflow builds all eleven targets and publishes the libraries, header, -checksums, and pinned manifest together. Release notes include consumer settings. -Existing releases are not overwritten; manual branch runs only upload workflow -artifacts. Older releases without a manifest cannot use download mode. - -Manifest generation uses the existing build recipes. To generate one locally, -place all target-named libraries and Android runtimes built from the current -revision in `artifacts/`, then run from the repository root: - -```sh -flutter pub get -dart --packages=.dart_tool/package_config.json tool/prebuilt_manifest.dart artifacts -``` - -Linux inspection requires `readelf`. Upload the manifest and libraries together. -Resolver tests run with the existing `flutter test` suite and CI. - ## Regenerate and verify the ABI The checked-in header is the ABI source for generated Dart declarations: diff --git a/dnix.json b/dnix.json new file mode 100644 index 0000000..406dec5 --- /dev/null +++ b/dnix.json @@ -0,0 +1,44 @@ +{ + "name": "flutter_libepiccash", + "repository": "https://github.com/cypherstack/flutter_libepiccash", + "library": "epic_cash_wallet", + "asset": "src/epic_cash_wallet_bindings.g.dart", + "rust": { + "workspace": "rust", + "crate": "epic-cash-wallet", + "outputHashes": { + "randomx-0.2.0": "sha256-bh9GvkkIQhYnEWmIFRW7psPqfamB8QDJ5yyJ1dxbP6k=" + }, + "env": { + "CXXSTDLIB_aarch64_linux_android": "static:-bundle=c++", + "CXXSTDLIB_armv7_linux_androideabi": "static:-bundle=c++", + "CXXSTDLIB_x86_64_linux_android": "static:-bundle=c++" + }, + "tools": [ + "cmake" + ] + }, + "toolchains": { + "rust": "1.99.1", + "xcode": "27.0" + }, + "floors": { + "macos": 12, + "ios": 13, + "android": 24, + "glibc": "2.28" + }, + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-apple-ios", + "aarch64-apple-ios-sim", + "x86_64-apple-ios", + "aarch64-linux-android", + "armv7-linux-androideabi", + "x86_64-linux-android", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-gnullvm" + ] +} diff --git a/dnix.lock b/dnix.lock new file mode 100644 index 0000000..8ccb68c --- /dev/null +++ b/dnix.lock @@ -0,0 +1,17 @@ +{ + "release": "ccc49989ad00", + "date": "2026-10-27", + "files": { + "aarch64-apple-darwin/libepic_cash_wallet.dylib": "0a7c8c9b831a36a7609d11deb36d0a31d058993fb4aeb1122f0f914f6545a447", + "aarch64-apple-ios-sim/libepic_cash_wallet.dylib": "87765aa07e4b0a53187330a2f2689ba44dd8a36900ddc98baff1ab7825c76f28", + "aarch64-apple-ios/libepic_cash_wallet.dylib": "0abb37b695b81f9cca74f89eaa751255bf710c0fe5ba94bb8a1b8de2bd4b8344", + "aarch64-linux-android/libepic_cash_wallet.so": "6404d615975dd014d5abc2b6e29c489ad8308343dd4585cf38cdd1a917f31eda", + "aarch64-unknown-linux-gnu/libepic_cash_wallet.so": "57ad01d7d780e66f02c3b218cc023301ecaaf5fe33afcafaad1400444bb2e510", + "armv7-linux-androideabi/libepic_cash_wallet.so": "1b58e0ee45c3661b165123b7cdd2ef38c7660aa384ca307236e7745445ba8ad0", + "x86_64-apple-darwin/libepic_cash_wallet.dylib": "b89164c1625022d5ef37b264cd121356f44cead282e0faed57d33bf6048c195f", + "x86_64-apple-ios/libepic_cash_wallet.dylib": "a57f674ad46b98c0deb04e6038090893ae9c972f084d30ed5fbd86da8ff40278", + "x86_64-linux-android/libepic_cash_wallet.so": "f29a24b3679298b6b22bcdc96fb8807b0569a452435fff55c54275f5dfd646c9", + "x86_64-pc-windows-gnullvm/epic_cash_wallet.dll": "3642bc87648ecbed119a28c9419938d78ba5d24023c6ef33562f5969354dd8a1", + "x86_64-unknown-linux-gnu/libepic_cash_wallet.so": "8b0fd9c8731e9e66053c193311b46bf7b175adb7f92936ff35abaaa4c1facc4c" + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index fc9f46d..f01d0e9 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -65,6 +65,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.3" + dnix: + dependency: transitive + description: + path: "." + ref: e91763ef25d084b3a5cf271a0cfee69159b1f779 + resolved-ref: e91763ef25d084b3a5cf271a0cfee69159b1f779 + url: "https://github.com/cypherstack/dnix.git" + source: git + version: "0.1.0" fake_async: dependency: transitive description: @@ -255,14 +264,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - native_toolchain_rust: - dependency: transitive - description: - name: native_toolchain_rust - sha256: "8469a0e32a47a5c84b7fd300de9fd0e8082aac76c37922986324f5082a58350d" - url: "https://pub.dev" - source: hosted - version: "1.0.6" numeric_keyboard: dependency: "direct main" description: @@ -327,14 +328,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" platform: dependency: transitive description: @@ -428,14 +421,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.12" - toml: - dependency: transitive - description: - name: toml - sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e" - url: "https://pub.dev" - source: hosted - version: "0.18.0" typed_data: dependency: transitive description: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..928b931 --- /dev/null +++ b/flake.lock @@ -0,0 +1,123 @@ +{ + "nodes": { + "dnix": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "zig-overlay": "zig-overlay" + }, + "locked": { + "lastModified": 1793124403, + "narHash": "sha256-Nf8+4VKQWYAE7telMnxGa2no18Yu4BemMZJ2PCeG+II=", + "owner": "cypherstack", + "repo": "dnix", + "rev": "e91763ef25d084b3a5cf271a0cfee69159b1f779", + "type": "github" + }, + "original": { + "owner": "cypherstack", + "repo": "dnix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1792502884, + "narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "02c087f3fe673c9e317f206a627cb6b910336dbe", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "dnix": "dnix" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "dnix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1792567368, + "narHash": "sha256-mrM+zFDRO1pI85t2SEq5R739BIq7Nv5XkFtVZK9nkZM=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "83dd732f2428827f35d997deae108a2ced5125f4", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "flake": false, + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig-overlay": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "dnix", + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1792585651, + "narHash": "sha256-Kg8Qdi6ur+K5hNJwav0pEqdylGd7vXxEPeBvaKW5nxA=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "98819174f1bb7c9961c3ebbb00d19fc4e1c1960d", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4ff975b --- /dev/null +++ b/flake.nix @@ -0,0 +1,10 @@ +{ + description = "Reproducible reference builds of flutter_libepiccash's Rust library"; + + inputs.dnix.url = "github:cypherstack/dnix"; + + outputs = { self, dnix, ... }: dnix.lib.package { + src = self; + config = ./dnix.json; + }; +} diff --git a/hook/build.dart b/hook/build.dart index 733f9b9..7997f79 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -1,299 +1,3 @@ -import 'dart:io'; +import 'package:dnix/dnix.dart'; -import 'package:code_assets/code_assets.dart'; -import 'package:hooks/hooks.dart'; -import 'package:native_toolchain_rust/native_toolchain_rust.dart'; - -import 'src/prebuilt.dart'; - -const _assetName = 'src/epic_cash_wallet_bindings.g.dart'; - -Future main(List args) async { - await build(args, (input, output) async { - if (!input.config.buildCodeAssets) { - return; - } - - final manifest = prebuiltManifest(input); - if (manifest != null) { - final directory = await downloadPrebuilt( - input, - output, - manifest, - target: _rustTarget(input.config.code), - walletName: _prebuiltFileName(input.config.code), - ); - try { - await _bundleDownloaded(input, output, directory.uri); - } finally { - await directory.delete(recursive: true); - } - return; - } - - // Cargo's dep-info lists Rust sources but omits these build inputs. - output.dependencies.addAll([ - for (final path in [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - ]) - input.packageRoot.resolve(path), - ]); - - await RustBuilder( - assetName: _assetName, - cratePath: 'rust', - extraCargoBuildArgs: const ['--locked'], - extraCargoEnvironmentVariables: { - ..._macOsRustToolchainEnvironment(), - ..._cargoEnvironment(input.config.code), - }, - ).run(input: input, output: output); - - if (input.config.code.targetOS == OS.android) { - await _bundleAndroidRuntime(input, output); - } - }); -} - -Future _bundleDownloaded( - BuildInput input, - BuildOutputBuilder output, - Uri directory, -) async { - final code = input.config.code; - await _bundleFile( - input, - output, - source: File.fromUri(directory.resolve(_prebuiltFileName(code))), - assetName: _assetName, - fileName: _bundledFileName(code.targetOS), - ); - if (code.targetOS == OS.android) { - await _bundleFile( - input, - output, - source: File.fromUri( - directory.resolve('libc++_shared-${_rustTarget(code)}.so'), - ), - assetName: 'libc++_shared.so', - fileName: 'libc++_shared.so', - ); - } -} - -Future _bundleAndroidRuntime( - BuildInput input, - BuildOutputBuilder output, -) async { - final code = input.config.code; - final target = _rustTarget(code); - final toolchainRoot = File.fromUri(code.cCompiler!.compiler).parent.parent; - final source = File.fromUri( - toolchainRoot.uri.resolve( - 'sysroot/usr/lib/${_androidSysrootTarget(target)}/libc++_shared.so', - ), - ); - if (!source.existsSync()) { - throw StateError('No NDK Android C++ runtime for $target: ${source.path}'); - } - output.dependencies.add(source.uri); - await _bundleFile( - input, - output, - source: source, - assetName: 'libc++_shared.so', - fileName: 'libc++_shared.so', - ); -} - -Future _bundleFile( - BuildInput input, - BuildOutputBuilder output, { - required File source, - required String assetName, - required String fileName, -}) async { - // Every architecture of one asset must have the same final basename. The - // release filenames contain target details, so copy the selected file to a - // canonical name in the hook output before handing it to Flutter. - final bundled = File.fromUri(input.outputDirectory.resolve(fileName)); - await bundled.parent.create(recursive: true); - await source.copy(bundled.path); - - output.assets.code.add( - CodeAsset( - package: input.packageName, - name: assetName, - linkMode: DynamicLoadingBundled(), - file: bundled.uri, - ), - ); -} - -Map _macOsRustToolchainEnvironment() { - if (!Platform.isMacOS) { - return const {}; - } - - // Preserve native_toolchain_rust's removal of Xcode-injected tool paths. - final paths = (Platform.environment['PATH'] ?? '') - .split(':') - .where((path) => !path.contains('Contents/Developer/')) - .toList(); - if (paths.any((path) => File('$path/rustc').existsSync())) { - return const {}; - } - - // Homebrew can expose only rustup on PATH. Its resolved installation also - // contains the rustc/cargo proxies needed by `rustup run ... cargo build`. - final homeDirectory = Platform.environment['HOME']; - for (final path in [ - ...paths, - if (homeDirectory != null) '$homeDirectory/.cargo/bin', - ]) { - final rustup = File('$path/rustup'); - if (!rustup.existsSync()) { - continue; - } - final toolchainBin = File(rustup.resolveSymbolicLinksSync()).parent.path; - if (File('$toolchainBin/rustc').existsSync()) { - return { - 'PATH': [...paths, toolchainBin].join(':'), - }; - } - } - return const {}; -} - -Map _cargoEnvironment(CodeConfig code) { - if (code.targetOS == OS.android) { - return _androidCargoEnvironment(code); - } - if (code.targetOS == OS.iOS) { - // Flutter 3.47 currently reports iOS 13 to native-assets hooks even when - // the consuming Xcode project has a newer deployment target. Keep the - // native library aligned with this package's documented iOS 15 minimum. - final targetVersion = - code.iOS.targetVersion < 15 ? 15 : code.iOS.targetVersion; - return {'IPHONEOS_DEPLOYMENT_TARGET': '$targetVersion.0'}; - } - if (code.targetOS == OS.macOS) { - // Build for the package floor. A library built for an older deployment - // target remains usable when the consuming application requires newer. - return {'MACOSX_DEPLOYMENT_TARGET': '12.0'}; - } - return const {}; -} - -Map _androidCargoEnvironment(CodeConfig code) { - final compiler = code.cCompiler; - if (compiler == null) { - throw StateError('Flutter did not provide an Android NDK compiler.'); - } - - final api = code.android.targetNdkApi; - if (api < 24) { - throw UnsupportedError( - 'Flutter 3.47 requires Android API 24 or newer; received API $api.', - ); - } - - final target = _rustTarget(code); - final environmentTarget = target.replaceAll('-', '_'); - final ndkTarget = - target == 'armv7-linux-androideabi' ? 'armv7a-linux-androideabi' : target; - final sysrootTarget = _androidSysrootTarget(target); - - final compilerDirectory = File.fromUri(compiler.compiler).parent; - final executableSuffix = Platform.isWindows ? '.cmd' : ''; - final clang = File( - '${compilerDirectory.path}${Platform.pathSeparator}' - '$ndkTarget$api-clang$executableSuffix', - ); - final clangPp = File( - '${compilerDirectory.path}${Platform.pathSeparator}' - '$ndkTarget$api-clang++$executableSuffix', - ); - if (!clang.existsSync() || !clangPp.existsSync()) { - throw StateError( - 'The configured Android NDK does not contain API $api compiler ' - 'wrappers for $target.', - ); - } - - final toolchainRoot = compilerDirectory.parent; - final sysroot = Directory.fromUri(toolchainRoot.uri.resolve('sysroot/')); - final targetInclude = Directory.fromUri( - sysroot.uri.resolve('usr/include/$sysrootTarget/'), - ); - - // bin//prebuilt/llvm/toolchains/ - var ndkRoot = compilerDirectory; - for (var level = 0; level < 5; level++) { - ndkRoot = ndkRoot.parent; - } - - return { - // native_toolchain_rust 1.0.6 currently defaults these entries to API 35. - // Override them with Flutter's consuming application's actual minimum API. - 'CC_$environmentTarget': clang.path, - 'CXX_$environmentTarget': clangPp.path, - 'CARGO_TARGET_${environmentTarget.toUpperCase()}_LINKER': clang.path, - 'BINDGEN_EXTRA_CLANG_ARGS_$environmentTarget': - '--sysroot=${_clangPathArgument(sysroot.path)} ' - '-I${_clangPathArgument(targetInclude.path)}', - 'ANDROID_NDK_HOME': ndkRoot.path, - // CMake-based Rust dependencies discover the NDK through this variable. - 'ANDROID_NDK_ROOT': ndkRoot.path, - }; -} - -String _androidSysrootTarget(String target) => - target == 'armv7-linux-androideabi' ? 'arm-linux-androideabi' : target; - -// Bindgen parses this environment variable with shlex, including on Windows. -// Normalize Windows separators and quote paths to preserve spaces and quotes. -String _clangPathArgument(String path) => - "'${path.replaceAll(r'\', '/').replaceAll("'", r"'\''")}'"; - -String _rustTarget(CodeConfig code) => - switch ((code.targetOS, code.targetArchitecture)) { - (OS.android, Architecture.arm) => 'armv7-linux-androideabi', - (OS.android, Architecture.arm64) => 'aarch64-linux-android', - (OS.android, Architecture.x64) => 'x86_64-linux-android', - (OS.iOS, Architecture.arm64) - when code.iOS.targetSdk == IOSSdk.iPhoneSimulator => - 'aarch64-apple-ios-sim', - (OS.iOS, Architecture.arm64) => 'aarch64-apple-ios', - (OS.iOS, Architecture.x64) => 'x86_64-apple-ios', - (OS.linux, Architecture.arm64) => 'aarch64-unknown-linux-gnu', - (OS.linux, Architecture.x64) => 'x86_64-unknown-linux-gnu', - (OS.macOS, Architecture.arm64) => 'aarch64-apple-darwin', - (OS.macOS, Architecture.x64) => 'x86_64-apple-darwin', - (OS.windows, Architecture.arm64) => 'aarch64-pc-windows-msvc', - (OS.windows, Architecture.x64) => 'x86_64-pc-windows-msvc', - _ => throw UnsupportedError( - 'Unsupported native target: ' - '${code.targetOS}/${code.targetArchitecture}', - ), - }; - -String _prebuiltFileName(CodeConfig code) { - final target = _rustTarget(code); - final extension = switch (code.targetOS) { - OS.android || OS.linux => 'so', - OS.iOS || OS.macOS => 'dylib', - OS.windows => 'dll', - _ => throw UnsupportedError('Unsupported OS: ${code.targetOS}'), - }; - return 'libepic_cash_wallet-$target.$extension'; -} - -String _bundledFileName(OS os) => switch (os) { - OS.android || OS.linux => 'libepic_cash_wallet.so', - OS.iOS || OS.macOS => 'libepic_cash_wallet.dylib', - OS.windows => 'epic_cash_wallet.dll', - _ => throw UnsupportedError('Unsupported OS: $os'), - }; +void main(List args) => buildNativeLibrary(args); diff --git a/hook/src/prebuilt.dart b/hook/src/prebuilt.dart deleted file mode 100644 index 01d4471..0000000 --- a/hook/src/prebuilt.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:code_assets/code_assets.dart'; -import 'package:crypto/crypto.dart'; -import 'package:hooks/hooks.dart'; - -import 'source_fingerprint.dart'; - -typedef ManifestPin = ({Uri url, String sha256}); -typedef _Library = ({String file, String sha256, int size}); -const _maximumBytes = 2 * 1024 * 1024 * 1024; -const _timeout = Duration(seconds: 30); - -ManifestPin? prebuiltManifest(BuildInput input) { - final defines = input.userDefines; - final mode = defines['native_build'] ?? 'source'; - if (mode != 'source' && mode != 'prebuilt') { - throw FormatException('native_build must be source or prebuilt.'); - } - if (mode == 'source') return null; - final url = defines['prebuilt_manifest_url']; - if (url is! String) throw FormatException('Missing prebuilt_manifest_url.'); - final uri = Uri.parse(url); - _requireHttps(uri); - return (url: uri, sha256: _digest(defines['prebuilt_manifest_sha256'])); -} - -String _digest(Object? value) { - if (value is! String || !RegExp(r'^[a-fA-F0-9]{64}$').hasMatch(value)) { - throw FormatException('Expected a SHA-256 digest (64 hex characters).'); - } - return value.toLowerCase(); -} - -void _requireHttps(Uri uri) { - if (uri.scheme != 'https' || - uri.host.isEmpty || - uri.userInfo.isNotEmpty || - uri.hasFragment) { - throw FormatException( - 'Prebuilt URLs require HTTPS without credentials or fragments.', - ); - } -} - -_Library _library(Object? value, String name) { - if (value is! Map || value['file'] != name) { - throw FormatException('Expected prebuilt library basename: $name'); - } - final size = value['size']; - if (size is! int || size <= 0 || size > _maximumBytes) { - throw FormatException('Invalid prebuilt size for $name.'); - } - return (file: name, sha256: _digest(value['sha256']), size: size); -} - -List<_Library> _select( - Object? manifest, - CodeConfig code, - String target, - String walletName, - String sourceHash, -) { - if (manifest is! Map || - manifest['schema_version'] != 1 || - manifest['package'] != 'flutter_libepiccash') { - throw FormatException('Unsupported flutter_libepiccash manifest.'); - } - if (_digest(manifest['source_sha256']) != sourceHash) { - throw StateError( - 'Prebuilt source fingerprint mismatch. Use a matching release or native_build: source.', - ); - } - final entries = manifest['artifacts']; - if (entries is! List || - entries.any((entry) => entry is! Map)) { - throw FormatException('Manifest artifacts must be a list of objects.'); - } - final matches = entries - .cast>() - .where( - (entry) => entry['target'] == target && entry['link_mode'] == 'dynamic', - ) - .toList(); - if (matches.length != 1) { - throw StateError('Expected exactly one dynamic prebuilt for $target.'); - } - final entry = matches.single; - final minimum = switch (code.targetOS) { - OS.android => code.android.targetNdkApi, - // Keep the existing source hook floor; Flutter can still report iOS 13. - OS.iOS => code.iOS.targetVersion < 15 ? 15 : code.iOS.targetVersion, - OS.macOS => code.macOS.targetVersion, - _ => null, - }; - if (minimum != null) { - final required = entry['minimum_os_version']; - if (required is! int || required <= 0) { - throw FormatException('Missing minimum_os_version for $target.'); - } - if (required > minimum) { - throw UnsupportedError( - 'Prebuilt requires OS/API $required; app targets $minimum.', - ); - } - } - if (code.targetOS == OS.linux) { - final glibc = entry['minimum_glibc_version']; - if (glibc is! String || !RegExp(r'^\d+\.\d+$').hasMatch(glibc)) { - throw FormatException( - 'Linux prebuilt must declare minimum_glibc_version.', - ); - } - } - final android = code.targetOS == OS.android; - final dependencies = entry['dependencies'] ?? []; - if (dependencies is! List || dependencies.length != (android ? 1 : 0)) { - throw FormatException( - 'Expected ${android ? 'one Android runtime' : 'no runtime dependencies'} for $target.', - ); - } - return [ - _library(entry, walletName), - if (android) _library(dependencies.single, 'libc++_shared-$target.so'), - ]; -} - -/// Return a verified directory for the existing bundler; the caller deletes it. -Future downloadPrebuilt( - BuildInput input, - BuildOutputBuilder output, - ManifestPin pin, { - required String target, - required String walletName, -}) async { - final code = input.config.code; - if (code.sanitizer != null || - code.linkModePreference == LinkModePreference.static) { - throw UnsupportedError( - 'Prebuilts require dynamic linking without sanitizers.', - ); - } - if (code.targetOS == OS.iOS && - code.iOS.targetSdk == IOSSdk.iPhoneOS && - code.targetArchitecture != Architecture.arm64) { - throw UnsupportedError('iOS device prebuilts require arm64.'); - } - final source = await sourceFingerprint(input.packageRoot); - output.dependencies.addAll(source.dependencies); - final outputDirectory = Directory.fromUri(input.outputDirectory); - await outputDirectory.create(recursive: true); - final directory = await outputDirectory.createTemp('.prebuilt-'); - final client = HttpClient() - ..autoUncompress = false - ..connectionTimeout = _timeout; - try { - final manifest = File.fromUri(directory.uri.resolve('manifest.json')); - await _download( - client, - pin.url, - manifest, - pin.sha256, - maximum: 1024 * 1024, - ); - final libraries = _select( - jsonDecode(await manifest.readAsString()), - code, - target, - walletName, - source.sha256, - ); - for (final library in libraries) { - await _download( - client, - pin.url.resolve(library.file), - File.fromUri(directory.uri.resolve(library.file)), - library.sha256, - size: library.size, - ); - } - return directory; - } catch (_) { - await directory.delete(recursive: true); - rethrow; - } finally { - client.close(force: true); - } -} - -Future _download( - HttpClient client, - Uri url, - File file, - String digest, { - int? size, - int maximum = _maximumBytes, -}) async { - final transfer = _transfer(client, url, file, size ?? maximum); - try { - await transfer.timeout(const Duration(minutes: 10)); - } on TimeoutException { - // Stop disk/network I/O before removing partial downloads on Windows. - client.close(force: true); - try { - await transfer; - } catch (_) { - /* Preserve the timeout. */ - } - rethrow; - } - if ((size != null && await file.length() != size) || - (await sha256.bind(file.openRead()).first).toString() != digest) { - throw StateError('Prebuilt integrity check failed: $url'); - } -} - -Future _transfer( - HttpClient client, - Uri uri, - File file, - int maximum, -) async { - for (var redirects = 0; redirects <= 5; redirects++) { - _requireHttps(uri); - final request = await client.getUrl(uri).timeout(_timeout); - request.followRedirects = false; - request.headers.set(HttpHeaders.acceptEncodingHeader, 'identity'); - final response = await request.close().timeout(_timeout); - if ([301, 302, 303, 307, 308].contains(response.statusCode)) { - final location = response.headers.value(HttpHeaders.locationHeader); - await response.listen(null).cancel(); - if (location == null) { - throw HttpException('Prebuilt redirect has no Location.'); - } - uri = uri.resolve(location); - continue; - } - if (response.statusCode != HttpStatus.ok || - response.contentLength > maximum) { - await response.listen(null).cancel(); - throw HttpException( - 'Invalid prebuilt response: HTTP ${response.statusCode}, length ${response.contentLength}.', - uri: uri, - ); - } - final sink = file.openWrite(); - var received = 0; - try { - await for (final bytes in response.timeout(_timeout)) { - received += bytes.length; - if (received > maximum) { - throw StateError('Prebuilt exceeds expected size.'); - } - sink.add(bytes); - await sink.flush(); - } - } finally { - await sink.close(); - } - return; - } - throw HttpException('Too many prebuilt redirects.'); -} diff --git a/hook/src/source_fingerprint.dart b/hook/src/source_fingerprint.dart deleted file mode 100644 index 1053efe..0000000 --- a/hook/src/source_fingerprint.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:crypto/crypto.dart' as crypto; - -/// Shared by the build hook and release tooling. Include paths as well as bytes -/// so moving, adding, or deleting native sources invalidates a release. -Future<({String sha256, List dependencies})> sourceFingerprint( - Uri packageRoot, -) async { - final files = {}; - final dependencies = []; - for (final path in [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - 'lib/src/epic_cash_wallet_bindings.g.dart', - 'rust/build.rs', - 'rust/cbindgen.toml', - 'hook/build.dart', - ]) { - files[path] = File.fromUri(packageRoot.resolve(path)); - } - for (final path in ['rust/src/', 'rust/include/']) { - final directory = Directory.fromUri(packageRoot.resolve(path)); - if (await FileSystemEntity.isLink(directory.path)) { - throw StateError('Prebuilt source inputs cannot be symlinks: $path'); - } - dependencies.add(directory.uri); - await for (final entity in directory.list( - recursive: true, - followLinks: false, - )) { - if (entity is Link) { - throw StateError( - 'Prebuilt source inputs cannot be symlinks: ${entity.path}', - ); - } - if (entity is Directory) dependencies.add(entity.uri); - if (entity is File) { - final relative = entity.uri.path.substring(packageRoot.path.length); - files[Uri.decodeComponent(relative)] = entity; - } - } - } - final paths = files.keys.toList()..sort(); - final framed = StringBuffer(); - for (final path in paths) { - final file = files[path]!; - if (await FileSystemEntity.isLink(file.path)) { - throw StateError('Prebuilt source inputs cannot be symlinks: $path'); - } - final digest = await crypto.sha256.bind(file.openRead()).first; - framed.write('$path\u0000$digest\n'); - dependencies.add(file.uri); - } - return ( - sha256: crypto.sha256.convert(utf8.encode(framed.toString())).toString(), - dependencies: dependencies, - ); -} diff --git a/pubspec.yaml b/pubspec.yaml index 62deb4c..d6f1dad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,13 +9,15 @@ environment: flutter: ">=3.47.0" dependencies: - crypto: ^3.0.7 code_assets: ^1.2.1 flutter: sdk: flutter ffi: ^2.2.0 hooks: ^2.1.0 - native_toolchain_rust: ^1.0.6 + dnix: + git: + url: https://github.com/cypherstack/dnix.git + ref: e91763ef25d084b3a5cf271a0cfee69159b1f779 decimal: ^2.1.0 mutex: ^3.0.0 uuid: ^4.0.0 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b3466a1..4bb43b8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1268,7 +1268,7 @@ dependencies = [ [[package]] name = "epic_core" version = "4.0.0" -source = "git+https://github.com/cypherstack/epic.git?branch=fix%2Frandomx-v0.2.0-api#c592407af7000c0105cf83a0bc7ec7849b9356f4" +source = "git+https://github.com/cypherstack/epic.git?branch=fix%2Frandomx-v0.2.0-api#21ac1683e9df755bf737624f8550e2f618c5d78b" dependencies = [ "blake2-rfc", "byteorder", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "epic_keychain" version = "4.0.0" -source = "git+https://github.com/cypherstack/epic.git?branch=fix%2Frandomx-v0.2.0-api#c592407af7000c0105cf83a0bc7ec7849b9356f4" +source = "git+https://github.com/cypherstack/epic.git?branch=fix%2Frandomx-v0.2.0-api#21ac1683e9df755bf737624f8550e2f618c5d78b" dependencies = [ "blake2-rfc", "byteorder", @@ -1394,7 +1394,7 @@ dependencies = [ [[package]] name = "epic_util" version = "4.0.0" -source = "git+https://github.com/cypherstack/epic.git?branch=fix%2Frandomx-v0.2.0-api#c592407af7000c0105cf83a0bc7ec7849b9356f4" +source = "git+https://github.com/cypherstack/epic.git?branch=fix%2Frandomx-v0.2.0-api#21ac1683e9df755bf737624f8550e2f618c5d78b" dependencies = [ "anyhow", "backtrace", @@ -4055,7 +4055,7 @@ dependencies = [ [[package]] name = "randomx" version = "0.2.0" -source = "git+https://github.com/cypherstack/randomx-rust#5147a05e320a9fb68306ae08a1e94bc9cdd56a77" +source = "git+https://github.com/cypherstack/randomx-rust#f030c36ab6ab8a8f5f69c3bb5a850f1464e10b5e" dependencies = [ "bindgen", "byteorder", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 38cdac4..c4492a1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -21,7 +21,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1" serde_derive = "1" simplelog = "^0.7.4" -openssl = { version = "0.10", features = ["vendored"] } zeroize = { version = "1.1.0", features = ["derive"] } rand = "0.6" reqwest = { version = "0.11", features = ["blocking", "json"] } @@ -53,6 +52,11 @@ anyhow = "1.0.69" # Pinned for compatibility with the current Epic wallet dependency graph. crunchy = "=0.2.2" +# Built from source where native-tls uses OpenSSL (Linux, Android); the other +# platforms use their own TLS and never link it. +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +openssl = { version = "0.10", features = ["vendored"] } + [dev-dependencies] hex = "0.4.3" cbindgen = "=0.24.3" diff --git a/test/build_hook_test.dart b/test/build_hook_test.dart deleted file mode 100644 index 899ae01..0000000 --- a/test/build_hook_test.dart +++ /dev/null @@ -1,482 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:code_assets/code_assets.dart'; -import 'package:crypto/crypto.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks/hooks.dart'; - -import '../hook/build.dart' as hook; -import '../hook/src/prebuilt.dart'; -import '../hook/src/source_fingerprint.dart'; -import '../tool/prebuilt_manifest.dart'; - -void main() { - late Directory workspace; - late Directory artifacts; - late Map manifest; - late Map> responses; - late HttpServer server; - late List requests; - late List<_LoopbackClient> clients; - int status = 200; - String? redirect; - bool stall = false; - - setUp(() async { - workspace = await Directory.systemTemp.createTemp('epic-prebuilt-'); - artifacts = await Directory.fromUri(workspace.uri.resolve('artifacts/')) - .create(); - responses = {}; - for (final target in releaseTargets.keys) { - for (final name in [ - releaseFileName(target), - if (target.contains('android')) 'libc++_shared-$target.so', - ]) { - final bytes = utf8.encode('fixture $name'); - responses[name] = bytes; - await File.fromUri(artifacts.uri.resolve(name)).writeAsBytes(bytes); - } - } - manifest = await writeManifest( - artifacts, - inspectGlibc: (_) async => '2.35', - ); - requests = []; - clients = []; - status = 200; - redirect = null; - stall = false; - server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); - server.listen((request) async { - final name = request.uri.pathSegments.last; - requests.add(name); - request.response.statusCode = status; - if (redirect != null) { - request.response.headers.set(HttpHeaders.locationHeader, redirect!); - } - request.response.bufferOutput = !stall; - request.response.add( - responses[name] ?? utf8.encode(jsonEncode(manifest)), - ); - if (stall) { - unawaited(request.response.done.catchError((_) {})); - } else { - await request.response.close(); - } - }); - }); - - tearDown(() async { - for (final client in clients) { - client.close(force: true); - } - await server.close(force: true); - await workspace.delete(recursive: true); - }); - - Future run({ - OS os = OS.macOS, - Architecture arch = Architecture.arm64, - IOSSdk sdk = IOSSdk.iPhoneOS, - int minimum = 24, - LinkModePreference linkMode = LinkModePreference.dynamic, - Map settings = const {}, - Future Function(BuildInput, BuildOutput)? check, - }) async { - // Construct outside HttpOverrides so the delegate remains a real client. - final client = _LoopbackClient(server.port); - clients.add(client); - await testCodeBuildHook( - mainMethod: (args) => HttpOverrides.runZoned( - () => hook.main(args), - createHttpClient: (_) => client, - ), - targetOS: os, - targetArchitecture: arch, - targetIOSSdk: sdk, - targetAndroidNdkApi: minimum, - targetIOSVersion: minimum, - targetMacOSVersion: minimum, - linkModePreference: linkMode, - userDefines: PackageUserDefines( - workspacePubspec: PackageUserDefinesSource( - basePath: workspace.uri, - defines: { - 'native_build': 'prebuilt', - 'prebuilt_manifest_url': 'https://release.invalid/manifest.json', - 'prebuilt_manifest_sha256': sha256 - .convert(utf8.encode(jsonEncode(manifest))) - .toString(), - ...settings, - }, - ), - ), - check: (input, output) async { - // Temporary downloads must not become hook dependencies or survive a run. - expect( - output.dependencies.any((uri) => uri.path.contains('.prebuilt-')), - isFalse, - ); - expect( - Directory.fromUri(input.outputDirectory) - .listSync() - .whereType(), - isEmpty, - ); - await check?.call(input, output); - }, - ); - } - - Map entry(String target) => (manifest['artifacts'] as List) - .cast>() - .singleWhere((entry) => entry['target'] == target); - - test( - 'producer hashes all wallets and Android runtimes and pins the manifest', - () async { - final entries = (manifest['artifacts'] as List) - .cast>(); - expect(entries, hasLength(11)); - for (final entry in entries) { - for (final file in [ - entry, - ...?entry['dependencies'] as List>?, - ]) { - final bytes = responses[file['file']]!; - expect(file['size'], bytes.length); - expect(file['sha256'], sha256.convert(bytes).toString()); - } - } - final document = File.fromUri(artifacts.uri.resolve('manifest.json')); - expect(jsonDecode(await document.readAsString()), manifest); - expect( - await File.fromUri(artifacts.uri.resolve('manifest.sha256')) - .readAsString(), - '${await fileSha256(document)} manifest.json\n', - ); - await expectLater(writeManifest(artifacts), throwsStateError); - await document.delete(); - await File.fromUri(artifacts.uri.resolve('manifest.sha256')).delete(); - await File.fromUri( - artifacts.uri.resolve('libc++_shared-aarch64-linux-android.so'), - ).delete(); - await expectLater( - writeManifest(artifacts, inspectGlibc: (_) async => '2.35'), - throwsStateError, - ); - expect(await document.exists(), isFalse); - }, - ); - - for (final target in [ - ( - OS.macOS, - Architecture.arm64, - IOSSdk.iPhoneOS, - 'aarch64-apple-darwin', - 'libepic_cash_wallet.dylib', - ), - ( - OS.windows, - Architecture.x64, - IOSSdk.iPhoneOS, - 'x86_64-pc-windows-msvc', - 'epic_cash_wallet.dll', - ), - ( - OS.linux, - Architecture.x64, - IOSSdk.iPhoneOS, - 'x86_64-unknown-linux-gnu', - 'libepic_cash_wallet.so', - ), - ( - OS.iOS, - Architecture.arm64, - IOSSdk.iPhoneOS, - 'aarch64-apple-ios', - 'libepic_cash_wallet.dylib', - ), - ( - OS.iOS, - Architecture.arm64, - IOSSdk.iPhoneSimulator, - 'aarch64-apple-ios-sim', - 'libepic_cash_wallet.dylib', - ), - ( - OS.iOS, - Architecture.x64, - IOSSdk.iPhoneSimulator, - 'x86_64-apple-ios', - 'libepic_cash_wallet.dylib', - ), - ( - OS.android, - Architecture.arm, - IOSSdk.iPhoneOS, - 'armv7-linux-androideabi', - 'libepic_cash_wallet.so', - ), - ( - OS.android, - Architecture.arm64, - IOSSdk.iPhoneOS, - 'aarch64-linux-android', - 'libepic_cash_wallet.so', - ), - ( - OS.android, - Architecture.x64, - IOSSdk.iPhoneOS, - 'x86_64-linux-android', - 'libepic_cash_wallet.so', - ), - ]) { - test('downloads and bundles ${target.$4} through the build hook', () async { - for (var attempt = 0; attempt < 2; attempt++) { - await run( - os: target.$1, - arch: target.$2, - sdk: target.$3, - minimum: target.$1 == OS.iOS ? 13 : 24, - check: (input, output) async { - final assets = { - for (final asset in output.assets.code) - asset.file!.pathSegments.last: asset, - }; - expect( - assets.keys, - unorderedEquals([ - target.$5, - if (target.$1 == OS.android) 'libc++_shared.so', - ]), - ); - final wallet = assets[target.$5]!; - expect( - wallet.id, - 'package:flutter_libepiccash/src/epic_cash_wallet_bindings.g.dart', - ); - expect(wallet.linkMode, isA()); - expect( - await File.fromUri(wallet.file!).readAsBytes(), - responses[releaseFileName(target.$4)], - ); - if (target.$1 == OS.android) { - final runtime = assets['libc++_shared.so']!; - expect( - runtime.id, - 'package:flutter_libepiccash/libc++_shared.so', - ); - expect( - await File.fromUri(runtime.file!).readAsBytes(), - responses['libc++_shared-${target.$4}.so'], - ); - } - }, - ); - } - expect(requests, [ - for (var i = 0; i < 2; i++) ...[ - 'manifest.json', - releaseFileName(target.$4), - if (target.$1 == OS.android) 'libc++_shared-${target.$4}.so', - ], - ]); - }); - } - - for (final mode in [null, 'source']) { - test('selects source builds with native_build: $mode', () async { - await testCodeBuildHook( - mainMethod: (args) => build(args, (input, output) async { - expect(prebuiltManifest(input), isNull); - }), - userDefines: PackageUserDefines( - workspacePubspec: PackageUserDefinesSource( - basePath: workspace.uri, - defines: {'native_build': ?mode}, - ), - ), - check: (_, _) {}, - ); - expect(requests, isEmpty); - }); - } - - for (final settings in >[ - {'native_build': 'invalid'}, - {'prebuilt_manifest_url': 'http://release.invalid/manifest.json'}, - { - 'prebuilt_manifest_url': - 'https://user:pass@release.invalid/manifest.json', - }, - {'prebuilt_manifest_sha256': null}, - {'prebuilt_manifest_sha256': 'g' * 64}, - ]) { - test('rejects invalid configuration: $settings', () async { - await expectLater(run(settings: settings), throwsFormatException); - expect(requests, isEmpty); - }); - } - - for (final file in [ - 'manifest.json', - 'libepic_cash_wallet-aarch64-linux-android.so', - 'libc++_shared-aarch64-linux-android.so', - ]) { - test('fails closed for corrupted $file', () async { - responses[file] = [1]; - await expectLater(run(os: OS.android), throwsStateError); - expect(requests.last, file); - }); - } - - test('rejects oversized downloads and HTTP errors', () async { - final wallet = releaseFileName('aarch64-apple-darwin'); - responses[wallet] = List.filled(responses[wallet]!.length + 1, 42); - await expectLater(run(), throwsStateError); - status = 404; - await expectLater(run(), throwsA(isA())); - }); - - test('follows HTTPS redirects but rejects downgrades and loops', () async { - status = 302; - redirect = 'https://release.invalid/loop'; - await expectLater(run(), throwsA(isA())); - expect(requests, hasLength(6)); - requests.clear(); - redirect = 'http://release.invalid/insecure'; - await expectLater(run(), throwsFormatException); - expect(requests, ['manifest.json']); - }); - - test('cancels timed-out downloads before cleanup', () async { - stall = true; - await runZoned( - () => expectLater(run(), throwsA(isA())), - zoneSpecification: ZoneSpecification( - createTimer: (self, parent, zone, duration, callback) { - return parent.createTimer( - zone, - duration == const Duration(minutes: 10) - ? const Duration(milliseconds: 100) - : duration, - callback, - ); - }, - ), - ); - expect(clients.single.forceClosed, isTrue); - }); - - for (final mutate in { - 'source mismatch': () => manifest['source_sha256'] = '0' * 64, - 'wrong package': () => manifest['package'] = 'other', - 'duplicate target': () => - (manifest['artifacts'] as List).add(entry('aarch64-apple-darwin')), - 'path traversal': () => - entry('aarch64-apple-darwin')['file'] = '../outside.dylib', - 'missing OS floor': () => - entry('aarch64-apple-darwin').remove('minimum_os_version'), - 'missing Android runtime': () => - entry('aarch64-linux-android').remove('dependencies'), - }.entries) { - test('rejects manifest ${mutate.key}', () async { - mutate.value(); - await expectLater( - run(os: mutate.key.contains('Android') ? OS.android : OS.macOS), - throwsA(anything), - ); - expect(requests, ['manifest.json']); - }); - } - - test('checks deployment minimums and required static linking', () async { - await expectLater(run(os: OS.android, minimum: 23), throwsUnsupportedError); - await expectLater( - run(linkMode: LinkModePreference.static), - throwsUnsupportedError, - ); - await expectLater( - run(os: OS.iOS, arch: Architecture.x64), - throwsUnsupportedError, - ); - await run(linkMode: LinkModePreference.preferStatic); - }); - - test( - 'fingerprints detect native edits, additions, renames, and symlinks', - () async { - final root = Directory.fromUri(workspace.uri.resolve('source/')); - for (final path in [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - 'rust/build.rs', - 'rust/cbindgen.toml', - 'rust/src/lib.rs', - 'rust/include/epic_cash_wallet.h', - 'lib/src/epic_cash_wallet_bindings.g.dart', - 'hook/build.dart', - ]) { - final file = File.fromUri(root.uri.resolve(path)); - await file.parent.create(recursive: true); - await file.writeAsString('fixture: $path'); - } - var fingerprint = await sourceFingerprint(root.uri); - for (final change in Function()>[ - () async { - await File.fromUri(root.uri.resolve('rust/build.rs')) - .writeAsString('changed'); - }, - () async { - await File.fromUri(root.uri.resolve('rust/src/new.rs')) - .writeAsString('new'); - }, - () async { - await File.fromUri(root.uri.resolve('rust/src/new.rs')) - .rename(root.uri.resolve('rust/src/renamed.rs').toFilePath()); - }, - ]) { - await change(); - final changed = await sourceFingerprint(root.uri); - expect(changed.sha256, isNot(fingerprint.sha256)); - fingerprint = changed; - } - expect(fingerprint.dependencies, contains(root.uri.resolve('rust/src/'))); - if (!Platform.isWindows) { - await Link.fromUri(root.uri.resolve('rust/src/link.rs')) - .create(root.uri.resolve('rust/build.rs').toFilePath()); - await expectLater(sourceFingerprint(root.uri), throwsStateError); - } - }, - ); -} - -// Route HTTPS test requests to loopback without weakening production URL checks. -class _LoopbackClient implements HttpClient { - final int port; - final HttpClient delegate = HttpClient(); - bool forceClosed = false; - _LoopbackClient(this.port); - @override - Future getUrl(Uri url) => delegate.getUrl( - url.replace(scheme: 'http', host: '127.0.0.1', port: port), - ); - @override - set connectionTimeout(Duration? value) => delegate.connectionTimeout = value; - @override - set autoUncompress(bool value) => delegate.autoUncompress = value; - @override - void close({bool force = false}) { - forceClosed |= force; - delegate.close(force: force); - } - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} diff --git a/tool/prebuilt_manifest.dart b/tool/prebuilt_manifest.dart deleted file mode 100644 index 4efb34e..0000000 --- a/tool/prebuilt_manifest.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:crypto/crypto.dart'; - -import '../hook/src/source_fingerprint.dart'; - -const releaseTargets = { - 'aarch64-linux-android': 24, - 'armv7-linux-androideabi': 24, - 'x86_64-linux-android': 24, - 'aarch64-apple-ios': 15, - 'aarch64-apple-ios-sim': 15, - 'x86_64-apple-ios': 15, - 'aarch64-unknown-linux-gnu': null, - 'x86_64-unknown-linux-gnu': null, - 'aarch64-apple-darwin': 12, - 'x86_64-apple-darwin': 12, - 'x86_64-pc-windows-msvc': null, -}; - -String releaseFileName(String target) { - if (!releaseTargets.containsKey(target)) { - throw ArgumentError.value(target, 'target', 'Unsupported release target'); - } - final extension = target.contains('apple') - ? 'dylib' - : target.contains('windows') - ? 'dll' - : 'so'; - return 'libepic_cash_wallet-$target.$extension'; -} - -Future fileSha256(File file) async => - (await sha256.bind(file.openRead()).first).toString(); - -/// Read the actual required symbol versions; do not guess from the build host. -Future minimumGlibc(File file) async { - final result = await Process.run('readelf', ['--version-info', file.path]); - if (result.exitCode != 0) { - throw StateError('readelf failed for ${file.path}: ${result.stderr}'); - } - final versions = RegExp(r'Name: GLIBC_(\d+)\.(\d+)\b') - .allMatches(result.stdout as String) - .map((match) => (int.parse(match[1]!), int.parse(match[2]!))) - .toList() - ..sort( - (a, b) => a.$1 != b.$1 ? a.$1.compareTo(b.$1) : a.$2.compareTo(b.$2), - ); - if (versions.isEmpty) { - throw StateError('No required glibc symbol versions in ${file.path}'); - } - final version = versions.last; - return '${version.$1}.${version.$2}'; -} - -/// Add a manifest to the existing release directory. All targets are required. -Future> writeManifest( - Directory directory, { - Future Function(File) inspectGlibc = minimumGlibc, -}) async { - final manifestFile = File.fromUri(directory.uri.resolve('manifest.json')); - final pinFile = File.fromUri(directory.uri.resolve('manifest.sha256')); - if (await manifestFile.exists() || await pinFile.exists()) { - throw StateError('Manifest output already exists.'); - } - Future> describe(String name) async { - final file = File.fromUri(directory.uri.resolve(name)); - if (await FileSystemEntity.type(file.path, followLinks: false) != - FileSystemEntityType.file) { - throw StateError('Missing or nonregular release artifact: $name'); - } - final size = await file.length(); - if (size <= 0 || size > 2 * 1024 * 1024 * 1024) { - throw StateError('Invalid release artifact size: $name'); - } - return {'file': name, 'size': size, 'sha256': await fileSha256(file)}; - } - - final artifacts = >[]; - final targets = releaseTargets.keys.toList()..sort(); - for (final target in targets) { - final name = releaseFileName(target); - artifacts.add({ - 'target': target, - 'link_mode': 'dynamic', - ...await describe(name), - if (releaseTargets[target] case final int minimum) - 'minimum_os_version': minimum, - if (target.endsWith('linux-gnu')) - 'minimum_glibc_version': await inspectGlibc( - File.fromUri(directory.uri.resolve(name)), - ), - if (target.contains('android')) - 'dependencies': [await describe('libc++_shared-$target.so')], - }); - } - final source = await sourceFingerprint(Directory.current.uri); - final manifest = { - 'schema_version': 1, - 'package': 'flutter_libepiccash', - 'source_sha256': source.sha256, - 'artifacts': artifacts, - }; - await manifestFile.writeAsString( - '${const JsonEncoder.withIndent(' ').convert(manifest)}\n', - ); - await pinFile.writeAsString( - '${await fileSha256(manifestFile)} manifest.json\n', - ); - return manifest; -} - -Future main(List args) async { - if (args.length != 1) { - stderr.writeln( - 'Usage: prebuilt_manifest.dart ', - ); - exitCode = 64; - return; - } - await writeManifest(Directory(args.single).absolute); -} diff --git a/tool/update_lock.dart b/tool/update_lock.dart new file mode 100644 index 0000000..7308919 --- /dev/null +++ b/tool/update_lock.dart @@ -0,0 +1,4 @@ +import 'dart:io'; +import 'package:dnix/tools.dart'; + +void main(List args) async => exit(await updateLock(args)); diff --git a/tool/verify_release.dart b/tool/verify_release.dart new file mode 100644 index 0000000..0dd9d24 --- /dev/null +++ b/tool/verify_release.dart @@ -0,0 +1,4 @@ +import 'dart:io'; +import 'package:dnix/tools.dart'; + +void main(List args) async => exit(await verifyRelease(args));