From 0e1f2d4a06b3f6f7bcad7a033ff9343c62eb9052 Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sun, 12 Jan 2025 16:35:27 +0800 Subject: [PATCH 1/7] feat: remove to blob in example --- webf/example/rust_builder/rust/src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/webf/example/rust_builder/rust/src/lib.rs b/webf/example/rust_builder/rust/src/lib.rs index f97c7efd7..2ac77a07d 100644 --- a/webf/example/rust_builder/rust/src/lib.rs +++ b/webf/example/rust_builder/rust/src/lib.rs @@ -63,12 +63,6 @@ pub extern "C" fn init_webf_app(handle: RustValue) println!("Async Storage Get Item Failed: {:?}", err); } } - - let html_element = context.document().document_element(); - let blob = html_element.to_blob(&exception_state).await.unwrap().unwrap(); - - let mut file = File::create("output.png").unwrap(); - file.write_all(&blob).unwrap(); }); let runtime_run_task_callback = Box::new(move || { From 91df194ab21d1ba2477ccc775f6e9ddfa6d7c41a Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sat, 18 Jan 2025 12:17:05 +0800 Subject: [PATCH 2/7] fix: run_rust_future_tasks_ not set --- bridge/core/executing_context.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bridge/core/executing_context.cc b/bridge/core/executing_context.cc index 03b1d76b9..fd5478665 100644 --- a/bridge/core/executing_context.cc +++ b/bridge/core/executing_context.cc @@ -397,6 +397,8 @@ void ExecutingContext::SetRunRustFutureTasks(const std::shared_ptrInvoke(this, 0, nullptr); } From 4e3d2663ed228c4c341c127de235517203426964 Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sat, 18 Jan 2025 17:20:42 +0800 Subject: [PATCH 3/7] feat: add cargokit in integration_tests --- .../linux/flutter/generated_plugins.cmake | 1 + integration_tests/pubspec.yaml | 2 + integration_tests/rust_builder/.gitignore | 29 ++ integration_tests/rust_builder/CHANGELOG.md | 3 + integration_tests/rust_builder/README.md | 1 + .../rust_builder/android/.gitignore | 9 + .../rust_builder/android/build.gradle | 54 +++ .../rust_builder/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../.github/workflows/check_and_lint.yml | 26 + .../workflows/test_example_plugin_build.yml | 86 ++++ .../rust_builder/cargokit/.gitignore | 4 + .../rust_builder/cargokit/LICENSE | 39 ++ .../rust_builder/cargokit/README | 8 + .../rust_builder/cargokit/build_pod.sh | 58 +++ .../cargokit/build_tool/README.md | 2 + .../cargokit/build_tool/analysis_options.yaml | 31 ++ .../cargokit/build_tool/bin/build_tool.dart | 5 + .../cargokit/build_tool/lib/build_tool.dart | 5 + .../lib/src/android_environment.dart | 192 ++++++++ .../lib/src/artifacts_provider.dart | 263 ++++++++++ .../build_tool/lib/src/build_cmake.dart | 37 ++ .../build_tool/lib/src/build_gradle.dart | 46 ++ .../build_tool/lib/src/build_pod.dart | 86 ++++ .../build_tool/lib/src/build_tool.dart | 268 +++++++++++ .../cargokit/build_tool/lib/src/builder.dart | 195 ++++++++ .../cargokit/build_tool/lib/src/cargo.dart | 45 ++ .../build_tool/lib/src/crate_hash.dart | 121 +++++ .../build_tool/lib/src/environment.dart | 65 +++ .../cargokit/build_tool/lib/src/logging.dart | 49 ++ .../cargokit/build_tool/lib/src/options.dart | 306 ++++++++++++ .../lib/src/precompile_binaries.dart | 199 ++++++++ .../cargokit/build_tool/lib/src/rustup.dart | 133 +++++ .../cargokit/build_tool/lib/src/target.dart | 137 ++++++ .../cargokit/build_tool/lib/src/util.dart | 169 +++++++ .../build_tool/lib/src/verify_binaries.dart | 81 ++++ .../cargokit/build_tool/pubspec.lock | 453 ++++++++++++++++++ .../cargokit/build_tool/pubspec.yaml | 30 ++ .../build_tool/test/builder_test.dart | 28 ++ .../cargokit/build_tool/test/cargo_test.dart | 28 ++ .../build_tool/test/options_test.dart | 75 +++ .../cargokit/build_tool/test/rustup_test.dart | 66 +++ .../cargokit/cmake/cargokit.cmake | 99 ++++ .../cargokit/cmake/resolve_symlinks.ps1 | 27 ++ .../cargokit/docs/architecture.md | 104 ++++ .../cargokit/docs/precompiled_binaries.md | 95 ++++ .../cargokit/gradle/plugin.gradle | 176 +++++++ .../rust_builder/cargokit/run_build_tool.cmd | 91 ++++ .../rust_builder/cargokit/run_build_tool.sh | 94 ++++ .../rust_builder/ios/Classes/dummy_file.c | 1 + .../ios/rust_native_api_tests.podspec | 45 ++ .../rust_builder/linux/CMakeLists.txt | 26 + .../rust_builder/macos/Classes/dummy_file.c | 1 + .../macos/rust_native_api_tests.podspec | 44 ++ integration_tests/rust_builder/pubspec.yaml | 49 ++ .../rust_builder/rust/.gitignore | 3 + .../rust_builder/rust/Cargo.toml | 13 + .../rust_builder/rust/src/lib.rs | 156 ++++++ .../rust_builder/windows/.gitignore | 17 + .../rust_builder/windows/CMakeLists.txt | 26 + .../windows/flutter/generated_plugins.cmake | 1 + 61 files changed, 4507 insertions(+) create mode 100644 integration_tests/rust_builder/.gitignore create mode 100644 integration_tests/rust_builder/CHANGELOG.md create mode 100644 integration_tests/rust_builder/README.md create mode 100644 integration_tests/rust_builder/android/.gitignore create mode 100644 integration_tests/rust_builder/android/build.gradle create mode 100644 integration_tests/rust_builder/android/settings.gradle create mode 100644 integration_tests/rust_builder/android/src/main/AndroidManifest.xml create mode 100644 integration_tests/rust_builder/cargokit/.github/workflows/check_and_lint.yml create mode 100644 integration_tests/rust_builder/cargokit/.github/workflows/test_example_plugin_build.yml create mode 100644 integration_tests/rust_builder/cargokit/.gitignore create mode 100644 integration_tests/rust_builder/cargokit/LICENSE create mode 100644 integration_tests/rust_builder/cargokit/README create mode 100755 integration_tests/rust_builder/cargokit/build_pod.sh create mode 100644 integration_tests/rust_builder/cargokit/build_tool/README.md create mode 100644 integration_tests/rust_builder/cargokit/build_tool/analysis_options.yaml create mode 100644 integration_tests/rust_builder/cargokit/build_tool/bin/build_tool.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/build_tool.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/android_environment.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/build_pod.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/build_tool.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/builder.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/cargo.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/environment.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/logging.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/options.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/rustup.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/target.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/util.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/pubspec.lock create mode 100644 integration_tests/rust_builder/cargokit/build_tool/pubspec.yaml create mode 100644 integration_tests/rust_builder/cargokit/build_tool/test/builder_test.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/test/cargo_test.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/test/options_test.dart create mode 100644 integration_tests/rust_builder/cargokit/build_tool/test/rustup_test.dart create mode 100644 integration_tests/rust_builder/cargokit/cmake/cargokit.cmake create mode 100644 integration_tests/rust_builder/cargokit/cmake/resolve_symlinks.ps1 create mode 100644 integration_tests/rust_builder/cargokit/docs/architecture.md create mode 100644 integration_tests/rust_builder/cargokit/docs/precompiled_binaries.md create mode 100644 integration_tests/rust_builder/cargokit/gradle/plugin.gradle create mode 100644 integration_tests/rust_builder/cargokit/run_build_tool.cmd create mode 100755 integration_tests/rust_builder/cargokit/run_build_tool.sh create mode 100644 integration_tests/rust_builder/ios/Classes/dummy_file.c create mode 100644 integration_tests/rust_builder/ios/rust_native_api_tests.podspec create mode 100644 integration_tests/rust_builder/linux/CMakeLists.txt create mode 100644 integration_tests/rust_builder/macos/Classes/dummy_file.c create mode 100644 integration_tests/rust_builder/macos/rust_native_api_tests.podspec create mode 100644 integration_tests/rust_builder/pubspec.yaml create mode 100644 integration_tests/rust_builder/rust/.gitignore create mode 100644 integration_tests/rust_builder/rust/Cargo.toml create mode 100644 integration_tests/rust_builder/rust/src/lib.rs create mode 100644 integration_tests/rust_builder/windows/.gitignore create mode 100644 integration_tests/rust_builder/windows/CMakeLists.txt diff --git a/integration_tests/linux/flutter/generated_plugins.cmake b/integration_tests/linux/flutter/generated_plugins.cmake index 3b18a0b7d..362d10e97 100644 --- a/integration_tests/linux/flutter/generated_plugins.cmake +++ b/integration_tests/linux/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + rust_native_api_tests ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/integration_tests/pubspec.yaml b/integration_tests/pubspec.yaml index 87f2da503..eca5a0030 100644 --- a/integration_tests/pubspec.yaml +++ b/integration_tests/pubspec.yaml @@ -30,6 +30,8 @@ dependencies: waterfall_flow: ^3.0.1 image_compare: ^1.1.2 card_swiper: ^3.0.1 + rust_native_api_tests: + path: rust_builder dev_dependencies: flutter_test: diff --git a/integration_tests/rust_builder/.gitignore b/integration_tests/rust_builder/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/integration_tests/rust_builder/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/integration_tests/rust_builder/CHANGELOG.md b/integration_tests/rust_builder/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/integration_tests/rust_builder/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/integration_tests/rust_builder/README.md b/integration_tests/rust_builder/README.md new file mode 100644 index 000000000..bc160a960 --- /dev/null +++ b/integration_tests/rust_builder/README.md @@ -0,0 +1 @@ +Please ignore this folder, which is just glue to build Rust with Flutter. diff --git a/integration_tests/rust_builder/android/.gitignore b/integration_tests/rust_builder/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/integration_tests/rust_builder/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/integration_tests/rust_builder/android/build.gradle b/integration_tests/rust_builder/android/build.gradle new file mode 100644 index 000000000..5692f87a1 --- /dev/null +++ b/integration_tests/rust_builder/android/build.gradle @@ -0,0 +1,54 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.integration_tests.rust_builder' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.integration_tests.rust_native_api_tests' + } + + // Bumping the plugin compileSdk version requires all clients of this plugin + // to bump the version in their app. + compileSdk 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + + +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + +apply from: "../cargokit/gradle/plugin.gradle" + +cargokit { + manifestDir = "../rust" + libname = "rust_native_api_tests" +} diff --git a/integration_tests/rust_builder/android/settings.gradle b/integration_tests/rust_builder/android/settings.gradle new file mode 100644 index 000000000..d82706841 --- /dev/null +++ b/integration_tests/rust_builder/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'rust_native_api_tests' diff --git a/integration_tests/rust_builder/android/src/main/AndroidManifest.xml b/integration_tests/rust_builder/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dae380075 --- /dev/null +++ b/integration_tests/rust_builder/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/integration_tests/rust_builder/cargokit/.github/workflows/check_and_lint.yml b/integration_tests/rust_builder/cargokit/.github/workflows/check_and_lint.yml new file mode 100644 index 000000000..adec80e1a --- /dev/null +++ b/integration_tests/rust_builder/cargokit/.github/workflows/check_and_lint.yml @@ -0,0 +1,26 @@ +on: + pull_request: + push: + branches: + - main + +name: Check and Lint + +jobs: + Flutter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #v2.7.0 + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d #1.6.0 + - name: Pub Get + run: dart pub get --no-precompile + working-directory: build_tool + - name: Dart Format + run: dart format . --output=none --set-exit-if-changed + working-directory: build_tool + - name: Analyze + run: dart analyze + working-directory: build_tool + - name: Test + run: dart test + working-directory: build_tool diff --git a/integration_tests/rust_builder/cargokit/.github/workflows/test_example_plugin_build.yml b/integration_tests/rust_builder/cargokit/.github/workflows/test_example_plugin_build.yml new file mode 100644 index 000000000..4fb0252dc --- /dev/null +++ b/integration_tests/rust_builder/cargokit/.github/workflows/test_example_plugin_build.yml @@ -0,0 +1,86 @@ +on: + pull_request: + push: + branches: + - main + +name: Test Example Plugin + +jobs: + Build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macOS-latest + - windows-latest + build_mode: + - debug + - profile + - release + env: + EXAMPLE_DIR: "a b/hello_rust_ffi_plugin/example" + CARGOKIT_VERBOSE: 1 + steps: + - name: Extract branch name + shell: bash + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + id: extract_branch + - name: Setup Repository + shell: bash + run: | + mkdir "a b" # Space is intentional + cd "a b" + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + # "advanced" branch has extra iOS flavor and uses rust nightly for release builds + git clone -b advanced https://github.com/irondash/hello_rust_ffi_plugin + cd hello_rust_ffi_plugin + git subtree pull --prefix cargokit https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }} ${{ steps.extract_branch.outputs.branch }} --squash + - uses: subosito/flutter-action@cc97e1648fff6ca5cc647fa67f47e70f7895510b # 2.11.0 + with: + channel: "stable" + - name: Install GTK + if: (matrix.os == 'ubuntu-latest') + run: sudo apt-get update && sudo apt-get install libgtk-3-dev + - name: Install ninja-build + if: (matrix.os == 'ubuntu-latest') + run: sudo apt-get update && sudo apt-get install ninja-build + - name: Build Linux (${{ matrix.build_mode }}) + if: matrix.os == 'ubuntu-latest' + shell: bash + working-directory: ${{ env.EXAMPLE_DIR }} + run: flutter build linux --${{ matrix.build_mode }} -v + - name: Build macOS (${{ matrix.build_mode }}) + if: matrix.os == 'macos-latest' + shell: bash + working-directory: ${{ env.EXAMPLE_DIR }} + run: flutter build macos --${{ matrix.build_mode }} -v + - name: Build iOS (${{ matrix.build_mode }}) + if: matrix.os == 'macos-latest' + shell: bash + working-directory: ${{ env.EXAMPLE_DIR }} + run: flutter build ios --${{ matrix.build_mode }} --no-codesign -v + - name: Build iOS (${{ matrix.build_mode }}) - flavor1 + if: matrix.os == 'macos-latest' + shell: bash + working-directory: ${{ env.EXAMPLE_DIR }} + run: flutter build ios --flavor flavor1 --${{ matrix.build_mode }} --no-codesign -v + - name: Build Windows (${{ matrix.build_mode }}) + if: matrix.os == 'windows-latest' + shell: bash + working-directory: ${{ env.EXAMPLE_DIR }} + run: flutter build windows --${{ matrix.build_mode }} -v + - name: Build Android (${{ matrix.build_mode }}) + shell: bash + working-directory: ${{ env.EXAMPLE_DIR }} + run: | + if [[ $(sysctl hw.optional.arm64) == *"hw.optional.arm64: 1"* ]]; then + export JAVA_HOME=$JAVA_HOME_17_arm64 + else + export JAVA_HOME=$JAVA_HOME_11_X64 + fi + flutter build apk --${{ matrix.build_mode }} -v + diff --git a/integration_tests/rust_builder/cargokit/.gitignore b/integration_tests/rust_builder/cargokit/.gitignore new file mode 100644 index 000000000..cf7bb868c --- /dev/null +++ b/integration_tests/rust_builder/cargokit/.gitignore @@ -0,0 +1,4 @@ +target +.dart_tool +*.iml +!pubspec.lock diff --git a/integration_tests/rust_builder/cargokit/LICENSE b/integration_tests/rust_builder/cargokit/LICENSE new file mode 100644 index 000000000..54a7d5893 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/LICENSE @@ -0,0 +1,39 @@ +Copyright 2022 Matej Knopp + +================================================================================ + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +================================================================================ + +APACHE LICENSE, VERSION 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/integration_tests/rust_builder/cargokit/README b/integration_tests/rust_builder/cargokit/README new file mode 100644 index 000000000..8ae4a073e --- /dev/null +++ b/integration_tests/rust_builder/cargokit/README @@ -0,0 +1,8 @@ +Experimental repository to provide glue for seamlessly integrating cargo build +with flutter plugins and packages. + +See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ +for a tutorial on how to use Cargokit. + +Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. + diff --git a/integration_tests/rust_builder/cargokit/build_pod.sh b/integration_tests/rust_builder/cargokit/build_pod.sh new file mode 100755 index 000000000..ed0e0d987 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_pod.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +BASEDIR=$(dirname "$0") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +BASEDIR=$(cd "$BASEDIR" ; pwd -P) + +# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project +NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` + +export PATH=${NEW_PATH%?} # remove trailing : + +env + +# Platform name (macosx, iphoneos, iphonesimulator) +export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME + +# Arctive architectures (arm64, armv7, x86_64), space separated. +export CARGOKIT_DARWIN_ARCHS=$ARCHS + +# Current build configuration (Debug, Release) +export CARGOKIT_CONFIGURATION=$CONFIGURATION + +# Path to directory containing Cargo.toml. +export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 + +# Temporary directory for build artifacts. +export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR + +# Output directory for final artifacts. +export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME + +# Directory to store built tool artifacts. +export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool + +# Directory inside root project. Not necessarily the top level directory of root project. +export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT + +FLUTTER_EXPORT_BUILD_ENVIRONMENT=( + "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS + "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS +) + +for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" +do + if [[ -f "$path" ]]; then + source "$path" + fi +done + +sh "$BASEDIR/run_build_tool.sh" build-pod "$@" + +# Make a symlink from built framework to phony file, which will be used as input to +# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate +# attribute on custom build phase) +ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" +ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/integration_tests/rust_builder/cargokit/build_tool/README.md b/integration_tests/rust_builder/cargokit/build_tool/README.md new file mode 100644 index 000000000..3816eca3a --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/integration_tests/rust_builder/cargokit/build_tool/analysis_options.yaml b/integration_tests/rust_builder/cargokit/build_tool/analysis_options.yaml new file mode 100644 index 000000000..a1aad5b3d --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/analysis_options.yaml @@ -0,0 +1,31 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + - prefer_relative_imports + - directives_ordering + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/integration_tests/rust_builder/cargokit/build_tool/bin/build_tool.dart b/integration_tests/rust_builder/cargokit/build_tool/bin/build_tool.dart new file mode 100644 index 000000000..f27ec75c3 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/bin/build_tool.dart @@ -0,0 +1,5 @@ +import 'package:build_tool/build_tool.dart' as build_tool; + +void main(List arguments) { + build_tool.runMain(arguments); +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/build_tool.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/build_tool.dart new file mode 100644 index 000000000..b329c01a3 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/build_tool.dart @@ -0,0 +1,5 @@ +import 'src/build_tool.dart' as build_tool; + +Future runMain(List args) async { + return build_tool.runMain(args); +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/android_environment.dart new file mode 100644 index 000000000..9342964b6 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/android_environment.dart @@ -0,0 +1,192 @@ +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; +import 'package:version/version.dart'; + +import 'target.dart'; +import 'util.dart'; + +class AndroidEnvironment { + AndroidEnvironment({ + required this.sdkPath, + required this.ndkVersion, + required this.minSdkVersion, + required this.targetTempDir, + required this.target, + }); + + static void clangLinkerWrapper(List args) { + final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; + if (clang == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); + } + final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; + if (target == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); + } + + runCommand(clang, [ + target, + ...args, + ]); + } + + /// Full path to Android SDK. + final String sdkPath; + + /// Full version of Android NDK. + final String ndkVersion; + + /// Minimum supported SDK version. + final int minSdkVersion; + + /// Target directory for build artifacts. + final String targetTempDir; + + /// Target being built. + final Target target; + + bool ndkIsInstalled() { + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); + return ndkPackageXml.existsSync(); + } + + void installNdk({ + required String javaHome, + }) { + final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; + final sdkManager = path.join( + sdkPath, + 'cmdline-tools', + 'latest', + 'bin', + 'sdkmanager$sdkManagerExtension', + ); + + log.info('Installing NDK $ndkVersion'); + runCommand(sdkManager, [ + '--install', + 'ndk;$ndkVersion', + ], environment: { + 'JAVA_HOME': javaHome, + }); + } + + Future> buildEnvironment() async { + final hostArch = Platform.isMacOS + ? "darwin-x86_64" + : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); + + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final toolchainPath = path.join( + ndkPath, + 'toolchains', + 'llvm', + 'prebuilt', + hostArch, + 'bin', + ); + + final minSdkVersion = + math.max(target.androidMinSdkVersion!, this.minSdkVersion); + + final exe = Platform.isWindows ? '.exe' : ''; + + final arKey = 'AR_${target.rust}'; + final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] + .map((e) => path.join(toolchainPath, e)) + .firstWhereOrNull((element) => File(element).existsSync()); + if (arValue == null) { + throw Exception('Failed to find ar for $target in $toolchainPath'); + } + + final targetArg = '--target=${target.rust}$minSdkVersion'; + + final ccKey = 'CC_${target.rust}'; + final ccValue = path.join(toolchainPath, 'clang$exe'); + final cfFlagsKey = 'CFLAGS_${target.rust}'; + final cFlagsValue = targetArg; + + final cxxKey = 'CXX_${target.rust}'; + final cxxValue = path.join(toolchainPath, 'clang++$exe'); + final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; + final cxxFlagsValue = targetArg; + + final linkerKey = + 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); + + final ranlibKey = 'RANLIB_${target.rust}'; + final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); + + final ndkVersionParsed = Version.parse(ndkVersion); + final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; + final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); + + final runRustTool = + Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; + + final packagePath = (await Isolate.resolvePackageUri( + Uri.parse('package:build_tool/buildtool.dart')))! + .toFilePath(); + final selfPath = path.canonicalize(path.join( + packagePath, + '..', + '..', + '..', + runRustTool, + )); + + // Make sure that run_build_tool is working properly even initially launched directly + // through dart run. + final toolTempDir = + Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; + + return { + arKey: arValue, + ccKey: ccValue, + cfFlagsKey: cFlagsValue, + cxxKey: cxxValue, + cxxFlagsKey: cxxFlagsValue, + ranlibKey: ranlibValue, + rustFlagsKey: rustFlagsValue, + linkerKey: selfPath, + // Recognized by main() so we know when we're acting as a wrapper + '_CARGOKIT_NDK_LINK_TARGET': targetArg, + '_CARGOKIT_NDK_LINK_CLANG': ccValue, + 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, + }; + } + + // Workaround for libgcc missing in NDK23, inspired by cargo-ndk + String _libGccWorkaround(String buildDir, Version ndkVersion) { + final workaroundDir = path.join( + buildDir, + 'cargokit', + 'libgcc_workaround', + '${ndkVersion.major}', + ); + Directory(workaroundDir).createSync(recursive: true); + if (ndkVersion.major >= 23) { + File(path.join(workaroundDir, 'libgcc.a')) + .writeAsStringSync('INPUT(-lunwind)'); + } else { + // Other way around, untested, forward libgcc.a from libunwind once Rust + // gets updated for NDK23+. + File(path.join(workaroundDir, 'libunwind.a')) + .writeAsStringSync('INPUT(-lgcc)'); + } + + var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; + if (rustFlags.isNotEmpty) { + rustFlags = '$rustFlags\x1f'; + } + rustFlags = '$rustFlags-L\x1f$workaroundDir'; + return rustFlags; + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart new file mode 100644 index 000000000..ef655a9ef --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart @@ -0,0 +1,263 @@ +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'builder.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'rustup.dart'; +import 'target.dart'; + +class Artifact { + /// File system location of the artifact. + final String path; + + /// Actual file name that the artifact should have in destination folder. + final String finalFileName; + + AritifactType get type { + if (finalFileName.endsWith('.dll') || + finalFileName.endsWith('.dll.lib') || + finalFileName.endsWith('.pdb') || + finalFileName.endsWith('.so') || + finalFileName.endsWith('.dylib')) { + return AritifactType.dylib; + } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { + return AritifactType.staticlib; + } else { + throw Exception('Unknown artifact type for $finalFileName'); + } + } + + Artifact({ + required this.path, + required this.finalFileName, + }); +} + +final _log = Logger('artifacts_provider'); + +class ArtifactProvider { + ArtifactProvider({ + required this.environment, + required this.userOptions, + }); + + final BuildEnvironment environment; + final CargokitUserOptions userOptions; + + Future>> getArtifacts(List targets) async { + final result = await _getPrecompiledArtifacts(targets); + + final pendingTargets = List.of(targets); + pendingTargets.removeWhere((element) => result.containsKey(element)); + + if (pendingTargets.isEmpty) { + return result; + } + + final rustup = Rustup(); + for (final target in targets) { + final builder = RustBuilder(target: target, environment: environment); + builder.prepare(rustup); + _log.info('Building ${environment.crateInfo.packageName} for $target'); + final targetDir = await builder.build(); + // For local build accept both static and dynamic libraries. + final artifactNames = { + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.dylib, + remote: false, + ), + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.staticlib, + remote: false, + ) + }; + final artifacts = artifactNames + .map((artifactName) => Artifact( + path: path.join(targetDir, artifactName), + finalFileName: artifactName, + )) + .where((element) => File(element.path).existsSync()) + .toList(); + result[target] = artifacts; + } + return result; + } + + Future>> _getPrecompiledArtifacts( + List targets) async { + if (userOptions.usePrecompiledBinaries == false) { + _log.info('Precompiled binaries are disabled'); + return {}; + } + if (environment.crateOptions.precompiledBinaries == null) { + _log.fine('Precompiled binaries not enabled for this crate'); + return {}; + } + + final start = Stopwatch()..start(); + final crateHash = CrateHash.compute(environment.manifestDir, + tempStorage: environment.targetTempDir); + _log.fine( + 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); + + final downloadedArtifactsDir = + path.join(environment.targetTempDir, 'precompiled', crateHash); + Directory(downloadedArtifactsDir).createSync(recursive: true); + + final res = >{}; + + for (final target in targets) { + final requiredArtifacts = getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + remote: true, + ); + final artifactsForTarget = []; + + for (final artifact in requiredArtifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final downloadedPath = path.join(downloadedArtifactsDir, fileName); + if (!File(downloadedPath).existsSync()) { + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + await _tryDownloadArtifacts( + crateHash: crateHash, + fileName: fileName, + signatureFileName: signatureFileName, + finalPath: downloadedPath, + ); + } + if (File(downloadedPath).existsSync()) { + artifactsForTarget.add(Artifact( + path: downloadedPath, + finalFileName: artifact, + )); + } else { + break; + } + } + + // Only provide complete set of artifacts. + if (artifactsForTarget.length == requiredArtifacts.length) { + _log.fine('Found precompiled artifacts for $target'); + res[target] = artifactsForTarget; + } + } + + return res; + } + + static Future _get(Uri url, {Map? headers}) async { + int attempt = 0; + const maxAttempts = 10; + while (true) { + try { + return await get(url, headers: headers); + } on SocketException catch (e) { + // Try to detect reset by peer error and retry. + if (attempt++ < maxAttempts && + (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { + _log.severe( + 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); + await Future.delayed(Duration(seconds: 1)); + continue; + } else { + rethrow; + } + } + } + } + + Future _tryDownloadArtifacts({ + required String crateHash, + required String fileName, + required String signatureFileName, + required String finalPath, + }) async { + final precompiledBinaries = environment.crateOptions.precompiledBinaries!; + final prefix = precompiledBinaries.uriPrefix; + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); + _log.fine('Downloading signature from $signatureUrl'); + final signature = await _get(signatureUrl); + if (signature.statusCode == 404) { + _log.warning( + 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); + return; + } + if (signature.statusCode != 200) { + _log.severe( + 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); + return; + } + _log.fine('Downloading binary from $url'); + final res = await _get(url); + if (res.statusCode != 200) { + _log.severe('Failed to download binary $url: status ${res.statusCode}'); + return; + } + if (verify( + precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { + File(finalPath).writeAsBytesSync(res.bodyBytes); + } else { + _log.shout('Signature verification failed! Ignoring binary.'); + } + } +} + +enum AritifactType { + staticlib, + dylib, +} + +AritifactType artifactTypeForTarget(Target target) { + if (target.darwinPlatform != null) { + return AritifactType.staticlib; + } else { + return AritifactType.dylib; + } +} + +List getArtifactNames({ + required Target target, + required String libraryName, + required bool remote, + AritifactType? aritifactType, +}) { + aritifactType ??= artifactTypeForTarget(target); + if (target.darwinArch != null) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.dylib']; + } + } else if (target.rust.contains('-windows-')) { + if (aritifactType == AritifactType.staticlib) { + return ['$libraryName.lib']; + } else { + return [ + '$libraryName.dll', + '$libraryName.dll.lib', + if (!remote) '$libraryName.pdb' + ]; + } + } else if (target.rust.contains('-linux-')) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.so']; + } + } else { + throw Exception("Unsupported target: ${target.rust}"); + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart new file mode 100644 index 000000000..9154371e0 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +class BuildCMake { + final CargokitUserOptions userOptions; + + BuildCMake({required this.userOptions}); + + Future build() async { + final targetPlatform = Environment.targetPlatform; + final target = Target.forFlutterName(Environment.targetPlatform); + if (target == null) { + throw Exception("Unknown target platform: $targetPlatform"); + } + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts([target]); + + final libs = artifacts[target]!; + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path) + .copySync(path.join(Environment.outputDir, lib.finalFileName)); + } + } + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart new file mode 100644 index 000000000..469c8b2d5 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +final log = Logger('build_gradle'); + +class BuildGradle { + BuildGradle({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.targetPlatforms.map((arch) { + final target = Target.forFlutterName(arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: true); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + for (final target in targets) { + final libs = artifacts[target]!; + final outputDir = path.join(Environment.outputDir, target.android!); + Directory(outputDir).createSync(recursive: true); + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); + } + } + } + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_pod.dart new file mode 100644 index 000000000..f01401e1c --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_pod.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +class BuildPod { + BuildPod({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.darwinArchs.map((arch) { + final target = Target.forDarwin( + platformName: Environment.darwinPlatformName, darwinAarch: arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + void performLipo(String targetFile, Iterable sourceFiles) { + runCommand("lipo", [ + '-create', + ...sourceFiles, + '-output', + targetFile, + ]); + } + + final outputDir = Environment.outputDir; + + Directory(outputDir).createSync(recursive: true); + + final staticLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.staticlib) + .toList(); + final dynamicLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.dylib) + .toList(); + + final libName = environment.crateInfo.packageName; + + // If there is static lib, use it and link it with pod + if (staticLibs.isNotEmpty) { + final finalTargetFile = path.join(outputDir, "lib$libName.a"); + performLipo(finalTargetFile, staticLibs.map((e) => e.path)); + } else { + // Otherwise try to replace bundle dylib with our dylib + final bundlePaths = [ + '$libName.framework/Versions/A/$libName', + '$libName.framework/$libName', + ]; + + for (final bundlePath in bundlePaths) { + final targetFile = path.join(outputDir, bundlePath); + if (File(targetFile).existsSync()) { + performLipo(targetFile, dynamicLibs.map((e) => e.path)); + + // Replace absolute id with @rpath one so that it works properly + // when moved to Frameworks. + runCommand("install_name_tool", [ + '-id', + '@rpath/$bundlePath', + targetFile, + ]); + return; + } + } + throw Exception('Unable to find bundle for dynamic library'); + } + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_tool.dart new file mode 100644 index 000000000..1d9462af7 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/build_tool.dart @@ -0,0 +1,268 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; + +import 'android_environment.dart'; +import 'build_cmake.dart'; +import 'build_gradle.dart'; +import 'build_pod.dart'; +import 'logging.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; +import 'util.dart'; +import 'verify_binaries.dart'; + +final log = Logger('build_tool'); + +abstract class BuildCommand extends Command { + Future runBuildCommand(CargokitUserOptions options); + + @override + Future run() async { + final options = CargokitUserOptions.load(); + + if (options.verboseLogging || + Platform.environment['CARGOKIT_VERBOSE'] == '1') { + enableVerboseLogging(); + } + + await runBuildCommand(options); + } +} + +class BuildPodCommand extends BuildCommand { + @override + final name = 'build-pod'; + + @override + final description = 'Build cocoa pod library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildPod(userOptions: options); + await build.build(); + } +} + +class BuildGradleCommand extends BuildCommand { + @override + final name = 'build-gradle'; + + @override + final description = 'Build android library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildGradle(userOptions: options); + await build.build(); + } +} + +class BuildCMakeCommand extends BuildCommand { + @override + final name = 'build-cmake'; + + @override + final description = 'Build CMake library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildCMake(userOptions: options); + await build.build(); + } +} + +class GenKeyCommand extends Command { + @override + final name = 'gen-key'; + + @override + final description = 'Generate key pair for signing precompiled binaries'; + + @override + void run() { + final kp = generateKey(); + final private = HEX.encode(kp.privateKey.bytes); + final public = HEX.encode(kp.publicKey.bytes); + print("Private Key: $private"); + print("Public Key: $public"); + } +} + +class PrecompileBinariesCommand extends Command { + PrecompileBinariesCommand() { + argParser + ..addOption( + 'repository', + mandatory: true, + help: 'Github repository slug in format owner/name', + ) + ..addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ) + ..addMultiOption('target', + help: 'Rust target triple of artifact to build.\n' + 'Can be specified multiple times or omitted in which case\n' + 'all targets for current platform will be built.') + ..addOption( + 'android-sdk-location', + help: 'Location of Android SDK (if available)', + ) + ..addOption( + 'android-ndk-version', + help: 'Android NDK version (if available)', + ) + ..addOption( + 'android-min-sdk-version', + help: 'Android minimum rquired version (if available)', + ) + ..addOption( + 'temp-dir', + help: 'Directory to store temporary build artifacts', + ) + ..addFlag( + "verbose", + abbr: "v", + defaultsTo: false, + help: "Enable verbose logging", + ); + } + + @override + final name = 'precompile-binaries'; + + @override + final description = 'Prebuild and upload binaries\n' + 'Private key must be passed through PRIVATE_KEY environment variable. ' + 'Use gen_key through generate priave key.\n' + 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; + + @override + Future run() async { + final verbose = argResults!['verbose'] as bool; + if (verbose) { + enableVerboseLogging(); + } + + final privateKeyString = Platform.environment['PRIVATE_KEY']; + if (privateKeyString == null) { + throw ArgumentError('Missing PRIVATE_KEY environment variable'); + } + final githubToken = Platform.environment['GITHUB_TOKEN']; + if (githubToken == null) { + throw ArgumentError('Missing GITHUB_TOKEN environment variable'); + } + final privateKey = HEX.decode(privateKeyString); + if (privateKey.length != 64) { + throw ArgumentError('Private key must be 64 bytes long'); + } + final manifestDir = argResults!['manifest-dir'] as String; + if (!Directory(manifestDir).existsSync()) { + throw ArgumentError('Manifest directory does not exist: $manifestDir'); + } + String? androidMinSdkVersionString = + argResults!['android-min-sdk-version'] as String?; + int? androidMinSdkVersion; + if (androidMinSdkVersionString != null) { + androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); + if (androidMinSdkVersion == null) { + throw ArgumentError( + 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); + } + } + final targetStrigns = argResults!['target'] as List; + final targets = targetStrigns.map((target) { + final res = Target.forRustTriple(target); + if (res == null) { + throw ArgumentError('Invalid target: $target'); + } + return res; + }).toList(growable: false); + final precompileBinaries = PrecompileBinaries( + privateKey: PrivateKey(privateKey), + githubToken: githubToken, + manifestDir: manifestDir, + repositorySlug: RepositorySlug.full(argResults!['repository'] as String), + targets: targets, + androidSdkLocation: argResults!['android-sdk-location'] as String?, + androidNdkVersion: argResults!['android-ndk-version'] as String?, + androidMinSdkVersion: androidMinSdkVersion, + tempDir: argResults!['temp-dir'] as String?, + ); + + await precompileBinaries.run(); + } +} + +class VerifyBinariesCommand extends Command { + VerifyBinariesCommand() { + argParser.addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ); + } + + @override + final name = "verify-binaries"; + + @override + final description = 'Verifies published binaries\n' + 'Checks whether there is a binary published for each targets\n' + 'and checks the signature.'; + + @override + Future run() async { + final manifestDir = argResults!['manifest-dir'] as String; + final verifyBinaries = VerifyBinaries( + manifestDir: manifestDir, + ); + await verifyBinaries.run(); + } +} + +Future runMain(List args) async { + try { + // Init logging before options are loaded + initLogging(); + + if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { + return AndroidEnvironment.clangLinkerWrapper(args); + } + + final runner = CommandRunner('build_tool', 'Cargokit built_tool') + ..addCommand(BuildPodCommand()) + ..addCommand(BuildGradleCommand()) + ..addCommand(BuildCMakeCommand()) + ..addCommand(GenKeyCommand()) + ..addCommand(PrecompileBinariesCommand()) + ..addCommand(VerifyBinariesCommand()); + + await runner.run(args); + } on ArgumentError catch (e) { + stderr.writeln(e.toString()); + exit(1); + } catch (e, s) { + log.severe(kDoubleSeparator); + log.severe('Cargokit BuildTool failed with error:'); + log.severe(kSeparator); + log.severe(e); + // This tells user to install Rust, there's no need to pollute the log with + // stack trace. + if (e is! RustupNotFoundException) { + log.severe(kSeparator); + log.severe(s); + log.severe(kSeparator); + log.severe('BuildTool arguments: $args'); + } + log.severe(kDoubleSeparator); + exit(1); + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/builder.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/builder.dart new file mode 100644 index 000000000..570a5375e --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/builder.dart @@ -0,0 +1,195 @@ +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'android_environment.dart'; +import 'cargo.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; +import 'util.dart'; + +final _log = Logger('builder'); + +enum BuildConfiguration { + debug, + release, + profile, +} + +extension on BuildConfiguration { + bool get isDebug => this == BuildConfiguration.debug; + String get rustName => switch (this) { + BuildConfiguration.debug => 'debug', + BuildConfiguration.release => 'release', + BuildConfiguration.profile => 'release', + }; +} + +class BuildException implements Exception { + final String message; + + BuildException(this.message); + + @override + String toString() { + return 'BuildException: $message'; + } +} + +class BuildEnvironment { + final BuildConfiguration configuration; + final CargokitCrateOptions crateOptions; + final String targetTempDir; + final String manifestDir; + final CrateInfo crateInfo; + + final bool isAndroid; + final String? androidSdkPath; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? javaHome; + + BuildEnvironment({ + required this.configuration, + required this.crateOptions, + required this.targetTempDir, + required this.manifestDir, + required this.crateInfo, + required this.isAndroid, + this.androidSdkPath, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.javaHome, + }); + + static BuildConfiguration parseBuildConfiguration(String value) { + // XCode configuration adds the flavor to configuration name. + final firstSegment = value.split('-').first; + final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( + (e) => e.name == firstSegment, + ); + if (buildConfiguration == null) { + _log.warning('Unknown build configuraiton $value, will assume release'); + return BuildConfiguration.release; + } + return buildConfiguration; + } + + static BuildEnvironment fromEnvironment({ + required bool isAndroid, + }) { + final buildConfiguration = + parseBuildConfiguration(Environment.configuration); + final manifestDir = Environment.manifestDir; + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + final crateInfo = CrateInfo.load(manifestDir); + return BuildEnvironment( + configuration: buildConfiguration, + crateOptions: crateOptions, + targetTempDir: Environment.targetTempDir, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: isAndroid, + androidSdkPath: isAndroid ? Environment.sdkPath : null, + androidNdkVersion: isAndroid ? Environment.ndkVersion : null, + androidMinSdkVersion: + isAndroid ? int.parse(Environment.minSdkVersion) : null, + javaHome: isAndroid ? Environment.javaHome : null, + ); + } +} + +class RustBuilder { + final Target target; + final BuildEnvironment environment; + + RustBuilder({ + required this.target, + required this.environment, + }); + + void prepare( + Rustup rustup, + ) { + final toolchain = _toolchain; + if (rustup.installedTargets(toolchain) == null) { + rustup.installToolchain(toolchain); + } + if (toolchain == 'nightly') { + rustup.installRustSrcForNightly(); + } + if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { + rustup.installTarget(target.rust, toolchain: toolchain); + } + } + + CargoBuildOptions? get _buildOptions => + environment.crateOptions.cargo[environment.configuration]; + + String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; + + /// Returns the path of directory containing build artifacts. + Future build() async { + final extraArgs = _buildOptions?.flags ?? []; + final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); + runCommand( + 'rustup', + [ + 'run', + _toolchain, + 'cargo', + 'build', + ...extraArgs, + '--manifest-path', + manifestPath, + '-p', + environment.crateInfo.packageName, + if (!environment.configuration.isDebug) '--release', + '--target', + target.rust, + '--target-dir', + environment.targetTempDir, + ], + environment: await _buildEnvironment(), + ); + return path.join( + environment.targetTempDir, + target.rust, + environment.configuration.rustName, + ); + } + + Future> _buildEnvironment() async { + if (target.android == null) { + return {}; + } else { + final sdkPath = environment.androidSdkPath; + final ndkVersion = environment.androidNdkVersion; + final minSdkVersion = environment.androidMinSdkVersion; + if (sdkPath == null) { + throw BuildException('androidSdkPath is not set'); + } + if (ndkVersion == null) { + throw BuildException('androidNdkVersion is not set'); + } + if (minSdkVersion == null) { + throw BuildException('androidMinSdkVersion is not set'); + } + final env = AndroidEnvironment( + sdkPath: sdkPath, + ndkVersion: ndkVersion, + minSdkVersion: minSdkVersion, + targetTempDir: environment.targetTempDir, + target: target, + ); + if (!env.ndkIsInstalled() && environment.javaHome != null) { + env.installNdk(javaHome: environment.javaHome!); + } + return env.buildEnvironment(); + } + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/cargo.dart new file mode 100644 index 000000000..0d4483ff5 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/cargo.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +class ManifestException { + ManifestException(this.message, {required this.fileName}); + + final String? fileName; + final String message; + + @override + String toString() { + if (fileName != null) { + return 'Failed to parse package manifest at $fileName: $message'; + } else { + return 'Failed to parse package manifest: $message'; + } + } +} + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + static CrateInfo parseManifest(String manifest, {final String? fileName}) { + final toml = TomlDocument.parse(manifest); + final package = toml.toMap()['package']; + if (package == null) { + throw ManifestException('Missing package section', fileName: fileName); + } + final name = package['name']; + if (name == null) { + throw ManifestException('Missing package name', fileName: fileName); + } + return CrateInfo(packageName: name); + } + + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + return parseManifest(manifest, fileName: manifestFile.path); + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart new file mode 100644 index 000000000..e58c37ff9 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; + +class CrateHash { + /// Computes a hash uniquely identifying crate content. This takes into account + /// content all all .rs files inside the src directory, as well as Cargo.toml, + /// Cargo.lock, build.rs and cargokit.yaml. + /// + /// If [tempStorage] is provided, computed hash is stored in a file in that directory + /// and reused on subsequent calls if the crate content hasn't changed. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({ + required this.manifestDir, + required this.tempStorage, + }); + + String _compute() { + final files = getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } else { + return _computeHash(files); + } + } + + /// Computes a quick hash based on files stat (without reading contents). This + /// is used to cache the real hash, which is slower to compute since it involves + /// reading every single file. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + // text Files are hashed by lines in case we're dealing with github checkout + // that auto-converts line endings. + final splitter = LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + + // Truncate to 128bits. + final hash = res.bytes.sublist(0, 16); + return hex.encode(hash); + } + + List getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList(); + files.sortBy((element) => element.path); + void addFile(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFile('Cargo.toml'); + addFile('Cargo.lock'); + addFile('build.rs'); + addFile('cargokit.yaml'); + return files; + } + + final String manifestDir; + final String? tempStorage; +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/environment.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/environment.dart new file mode 100644 index 000000000..1d267edb1 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/environment.dart @@ -0,0 +1,65 @@ +import 'dart:io'; + +extension on String { + String resolveSymlink() => File(this).resolveSymbolicLinksSync(); +} + +class Environment { + /// Current build configuration (debug or release). + static String get configuration => + _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); + + static bool get isDebug => configuration == 'debug'; + static bool get isRelease => configuration == 'release'; + + /// Temporary directory where Rust build artifacts are placed. + static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); + + /// Final output directory where the build artifacts are placed. + static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); + + /// Path to the crate manifest (containing Cargo.toml). + static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); + + /// Directory inside root project. Not necessarily root folder. Symlinks are + /// not resolved on purpose. + static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); + + // Pod + + /// Platform name (macosx, iphoneos, iphonesimulator). + static String get darwinPlatformName => + _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); + + /// List of architectures to build for (arm64, armv7, x86_64). + static List get darwinArchs => + _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); + + // Gradle + static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); + static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); + static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); + static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); + static List get targetPlatforms => + _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); + + // CMAKE + static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); + + static String _getEnv(String key) { + final res = Platform.environment[key]; + if (res == null) { + throw Exception("Missing environment variable $key"); + } + return res; + } + + static String _getEnvPath(String key) { + final res = _getEnv(key); + if (Directory(res).existsSync()) { + return res.resolveSymlink(); + } else { + return res; + } + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/logging.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/logging.dart new file mode 100644 index 000000000..06392b993 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/logging.dart @@ -0,0 +1,49 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; + +const String kSeparator = "--"; +const String kDoubleSeparator = "=="; + +bool _lastMessageWasSeparator = false; + +void _log(LogRecord rec) { + final prefix = '${rec.level.name}: '; + final out = rec.level == Level.SEVERE ? stderr : stdout; + if (rec.message == kSeparator) { + if (!_lastMessageWasSeparator) { + out.write(prefix); + out.writeln('-' * 80); + _lastMessageWasSeparator = true; + } + return; + } else if (rec.message == kDoubleSeparator) { + out.write(prefix); + out.writeln('=' * 80); + _lastMessageWasSeparator = true; + return; + } + out.write(prefix); + out.writeln(rec.message); + _lastMessageWasSeparator = false; +} + +void initLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((LogRecord rec) { + final lines = rec.message.split('\n'); + for (final line in lines) { + if (line.isNotEmpty || lines.length == 1 || line != lines.last) { + _log(LogRecord( + rec.level, + line, + rec.loggerName, + )); + } + } + }); +} + +void enableVerboseLogging() { + Logger.root.level = Level.ALL; +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/options.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/options.dart new file mode 100644 index 000000000..7937dcacd --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/options.dart @@ -0,0 +1,306 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'builder.dart'; +import 'environment.dart'; +import 'rustup.dart'; + +final _log = Logger('options'); + +/// A class for exceptions that have source span information attached. +class SourceSpanException implements Exception { + // This is a getter so that subclasses can override it. + /// A message describing the exception. + String get message => _message; + final String _message; + + // This is a getter so that subclasses can override it. + /// The span associated with this exception. + /// + /// This may be `null` if the source location can't be determined. + SourceSpan? get span => _span; + final SourceSpan? _span; + + SourceSpanException(this._message, this._span); + + /// Returns a string representation of `this`. + /// + /// [color] may either be a [String], a [bool], or `null`. If it's a string, + /// it indicates an ANSI terminal color escape that should be used to + /// highlight the span's text. If it's `true`, it indicates that the text + /// should be highlighted using the default color. If it's `false` or `null`, + /// it indicates that the text shouldn't be highlighted. + @override + String toString({Object? color}) { + if (span == null) return message; + return 'Error on ${span!.message(message, color: color)}'; + } +} + +enum Toolchain { + stable, + beta, + nightly, +} + +class CargoBuildOptions { + final Toolchain toolchain; + final List flags; + + CargoBuildOptions({ + required this.toolchain, + required this.flags, + }); + + static Toolchain _toolchainFromNode(YamlNode node) { + if (node case YamlScalar(value: String name)) { + final toolchain = + Toolchain.values.firstWhereOrNull((element) => element.name == name); + if (toolchain != null) { + return toolchain; + } + } + throw SourceSpanException( + 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', + node.span); + } + + static CargoBuildOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + Toolchain toolchain = Toolchain.stable; + List flags = []; + for (final MapEntry(:key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: 'toolchain')) { + toolchain = _toolchainFromNode(value); + } else if (key case YamlScalar(value: 'extra_flags')) { + if (value case YamlList(nodes: List list)) { + if (list.every((element) { + if (element case YamlScalar(value: String _)) { + return true; + } + return false; + })) { + flags = list.map((e) => e.value as String).toList(); + continue; + } + } + throw SourceSpanException( + 'Extra flags must be a list of strings', value.span); + } else { + throw SourceSpanException( + 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', + key.span); + } + } + return CargoBuildOptions(toolchain: toolchain, flags: flags); + } +} + +extension on YamlMap { + /// Map that extracts keys so that we can do map case check on them. + Map get valueMap => + nodes.map((key, value) => MapEntry(key.value, value)); +} + +class PrecompiledBinaries { + final String uriPrefix; + final PublicKey publicKey; + + PrecompiledBinaries({ + required this.uriPrefix, + required this.publicKey, + }); + + static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { + final bytes = HEX.decode(key); + if (bytes.length != 32) { + throw SourceSpanException( + 'Invalid public key. Must be 32 bytes long.', span); + } + return PublicKey(bytes); + } + + static PrecompiledBinaries parse(YamlNode node) { + if (node case YamlMap(valueMap: Map map)) { + if (map + case { + 'url_prefix': YamlNode urlPrefixNode, + 'public_key': YamlNode publicKeyNode, + }) { + final urlPrefix = switch (urlPrefixNode) { + YamlScalar(value: String urlPrefix) => urlPrefix, + _ => throw SourceSpanException( + 'Invalid URL prefix value.', urlPrefixNode.span), + }; + final publicKey = switch (publicKeyNode) { + YamlScalar(value: String publicKey) => + _publicKeyFromHex(publicKey, publicKeyNode.span), + _ => throw SourceSpanException( + 'Invalid public key value.', publicKeyNode.span), + }; + return PrecompiledBinaries( + uriPrefix: urlPrefix, + publicKey: publicKey, + ); + } + } + throw SourceSpanException( + 'Invalid precompiled binaries value. ' + 'Expected Map with "url_prefix" and "public_key".', + node.span); + } +} + +/// Cargokit options specified for Rust crate. +class CargokitCrateOptions { + CargokitCrateOptions({ + this.cargo = const {}, + this.precompiledBinaries, + }); + + final Map cargo; + final PrecompiledBinaries? precompiledBinaries; + + static CargokitCrateOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + final options = {}; + PrecompiledBinaries? precompiledBinaries; + + for (final entry in node.nodes.entries) { + if (entry + case MapEntry( + key: YamlScalar(value: 'cargo'), + value: YamlNode node, + )) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: String name)) { + final configuration = BuildConfiguration.values + .firstWhereOrNull((element) => element.name == name); + if (configuration != null) { + options[configuration] = CargoBuildOptions.parse(value); + continue; + } + } + throw SourceSpanException( + 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', + key.span); + } + } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { + precompiledBinaries = PrecompiledBinaries.parse(entry.value); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', + entry.key.span); + } + } + return CargokitCrateOptions( + cargo: options, + precompiledBinaries: precompiledBinaries, + ); + } + + static CargokitCrateOptions load({ + required String manifestDir, + }) { + final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); + final file = File.fromUri(uri); + if (file.existsSync()) { + final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); + return parse(contents); + } else { + return CargokitCrateOptions(); + } + } +} + +class CargokitUserOptions { + // When Rustup is installed always build locally unless user opts into + // using precompiled binaries. + static bool defaultUsePrecompiledBinaries() { + return Rustup.executablePath() == null; + } + + CargokitUserOptions({ + required this.usePrecompiledBinaries, + required this.verboseLogging, + }); + + CargokitUserOptions._() + : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), + verboseLogging = false; + + static CargokitUserOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); + bool verboseLogging = false; + + for (final entry in node.nodes.entries) { + if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { + if (entry.value case YamlScalar(value: bool value)) { + usePrecompiledBinaries = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "use_precompiled_binaries". Must be a boolean.', + entry.value.span); + } else if (entry.key case YamlScalar(value: 'verbose_logging')) { + if (entry.value case YamlScalar(value: bool value)) { + verboseLogging = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "verbose_logging". Must be a boolean.', + entry.value.span); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', + entry.key.span); + } + } + return CargokitUserOptions( + usePrecompiledBinaries: usePrecompiledBinaries, + verboseLogging: verboseLogging, + ); + } + + static CargokitUserOptions load() { + String fileName = "cargokit_options.yaml"; + var userProjectDir = Directory(Environment.rootProjectDir); + + while (userProjectDir.parent.path != userProjectDir.path) { + final configFile = File(path.join(userProjectDir.path, fileName)); + if (configFile.existsSync()) { + final contents = loadYamlNode( + configFile.readAsStringSync(), + sourceUrl: configFile.uri, + ); + final res = parse(contents); + if (res.verboseLogging) { + _log.info('Found user options file at ${configFile.path}'); + } + return res; + } + userProjectDir = userProjectDir.parent; + } + return CargokitUserOptions._(); + } + + final bool usePrecompiledBinaries; + final bool verboseLogging; +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart new file mode 100644 index 000000000..39ffafc45 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart @@ -0,0 +1,199 @@ +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; + +final _log = Logger('precompile_binaries'); + +class PrecompileBinaries { + PrecompileBinaries({ + required this.privateKey, + required this.githubToken, + required this.repositorySlug, + required this.manifestDir, + required this.targets, + this.androidSdkLocation, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.tempDir, + }); + + final PrivateKey privateKey; + final String githubToken; + final RepositorySlug repositorySlug; + final String manifestDir; + final List targets; + final String? androidSdkLocation; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? tempDir; + + static String fileName(Target target, String name) { + return '${target.rust}_$name'; + } + + static String signatureFileName(Target target, String name) { + return '${target.rust}_$name.sig'; + } + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final targets = List.of(this.targets); + if (targets.isEmpty) { + targets.addAll([ + ...Target.buildableTargets(), + if (androidSdkLocation != null) ...Target.androidTargets(), + ]); + } + + _log.info('Precompiling binaries for $targets'); + + final hash = CrateHash.compute(manifestDir); + _log.info('Computed crate hash: $hash'); + + final String tagName = 'precompiled_$hash'; + + final github = GitHub(auth: Authentication.withToken(githubToken)); + final repo = github.repositories; + final release = await _getOrCreateRelease( + repo: repo, + tagName: tagName, + packageName: crateInfo.packageName, + hash: hash, + ); + + final tempDir = this.tempDir != null + ? Directory(this.tempDir!) + : Directory.systemTemp.createTempSync('precompiled_'); + + tempDir.createSync(recursive: true); + + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + + final buildEnvironment = BuildEnvironment( + configuration: BuildConfiguration.release, + crateOptions: crateOptions, + targetTempDir: tempDir.path, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: androidSdkLocation != null, + androidSdkPath: androidSdkLocation, + androidNdkVersion: androidNdkVersion, + androidMinSdkVersion: androidMinSdkVersion, + ); + + final rustup = Rustup(); + + for (final target in targets) { + final artifactNames = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + if (artifactNames.every((name) { + final fileName = PrecompileBinaries.fileName(target, name); + return (release.assets ?? []).any((e) => e.name == fileName); + })) { + _log.info("All artifacts for $target already exist - skipping"); + continue; + } + + _log.info('Building for $target'); + + final builder = + RustBuilder(target: target, environment: buildEnvironment); + builder.prepare(rustup); + final res = await builder.build(); + + final assets = []; + for (final name in artifactNames) { + final file = File(path.join(res, name)); + if (!file.existsSync()) { + throw Exception('Missing artifact: ${file.path}'); + } + + final data = file.readAsBytesSync(); + final create = CreateReleaseAsset( + name: PrecompileBinaries.fileName(target, name), + contentType: "application/octet-stream", + assetData: data, + ); + final signature = sign(privateKey, data); + final signatureCreate = CreateReleaseAsset( + name: signatureFileName(target, name), + contentType: "application/octet-stream", + assetData: signature, + ); + bool verified = verify(public(privateKey), data, signature); + if (!verified) { + throw Exception('Signature verification failed'); + } + assets.add(create); + assets.add(signatureCreate); + } + _log.info('Uploading assets: ${assets.map((e) => e.name)}'); + for (final asset in assets) { + // This seems to be failing on CI so do it one by one + int retryCount = 0; + while (true) { + try { + await repo.uploadReleaseAssets(release, [asset]); + break; + } on Exception catch (e) { + if (retryCount == 10) { + rethrow; + } + ++retryCount; + _log.shout( + 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); + await Future.delayed(Duration(seconds: 2)); + } + } + } + } + + _log.info('Cleaning up'); + tempDir.deleteSync(recursive: true); + } + + Future _getOrCreateRelease({ + required RepositoriesService repo, + required String tagName, + required String packageName, + required String hash, + }) async { + Release release; + try { + _log.info('Fetching release $tagName'); + release = await repo.getReleaseByTagName(repositorySlug, tagName); + } on ReleaseNotFound { + _log.info('Release not found - creating release $tagName'); + release = await repo.createRelease( + repositorySlug, + CreateRelease.from( + tagName: tagName, + name: 'Precompiled binaries ${hash.substring(0, 8)}', + targetCommitish: null, + isDraft: false, + isPrerelease: false, + body: 'Precompiled binaries for crate $packageName, ' + 'crate hash $hash.', + )); + } + return release; + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/rustup.dart new file mode 100644 index 000000000..f284179a4 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/rustup.dart @@ -0,0 +1,133 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; + +import 'util.dart'; + +class _Toolchain { + _Toolchain( + this.name, + this.targets, + ); + + final String name; + final List targets; +} + +class Rustup { + List? installedTargets(String toolchain) { + final targets = _installedTargets(toolchain); + return targets != null ? List.unmodifiable(targets) : null; + } + + void installToolchain(String toolchain) { + log.info("Installing Rust toolchain: $toolchain"); + runCommand("rustup", ['toolchain', 'install', toolchain]); + _installedToolchains + .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); + } + + void installTarget( + String target, { + required String toolchain, + }) { + log.info("Installing Rust target: $target"); + runCommand("rustup", [ + 'target', + 'add', + '--toolchain', + toolchain, + target, + ]); + _installedTargets(toolchain)?.add(target); + } + + final List<_Toolchain> _installedToolchains; + + Rustup() : _installedToolchains = _getInstalledToolchains(); + + List? _installedTargets(String toolchain) => _installedToolchains + .firstWhereOrNull( + (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) + ?.targets; + + static List<_Toolchain> _getInstalledToolchains() { + String extractToolchainName(String line) { + // ignore (default) after toolchain name + final parts = line.split(' '); + return parts[0]; + } + + final res = runCommand("rustup", ['toolchain', 'list']); + + // To list all non-custom toolchains, we need to filter out lines that + // don't start with "stable", "beta", or "nightly". + Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) + .map(extractToolchainName) + .toList(growable: true); + + return lines + .map( + (name) => _Toolchain( + name, + _getInstalledTargets(name), + ), + ) + .toList(growable: true); + } + + static List _getInstalledTargets(String toolchain) { + final res = runCommand("rustup", [ + 'target', + 'list', + '--toolchain', + toolchain, + '--installed', + ]); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty) + .toList(growable: true); + return lines; + } + + bool _didInstallRustSrcForNightly = false; + + void installRustSrcForNightly() { + if (_didInstallRustSrcForNightly) { + return; + } + // Useful for -Z build-std + runCommand( + "rustup", + ['component', 'add', 'rust-src', '--toolchain', 'nightly'], + ); + _didInstallRustSrcForNightly = true; + } + + static String? executablePath() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + final rustupPath = path.join(p, rustup); + if (File(rustupPath).existsSync()) { + return rustupPath; + } + } + return null; + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/target.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/target.dart new file mode 100644 index 000000000..9287b23c7 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/target.dart @@ -0,0 +1,137 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import 'util.dart'; + +class Target { + Target({ + required this.rust, + this.flutter, + this.android, + this.androidMinSdkVersion, + this.darwinPlatform, + this.darwinArch, + }); + + static final all = [ + Target( + rust: 'armv7-linux-androideabi', + flutter: 'android-arm', + android: 'armeabi-v7a', + androidMinSdkVersion: 16, + ), + Target( + rust: 'aarch64-linux-android', + flutter: 'android-arm64', + android: 'arm64-v8a', + androidMinSdkVersion: 21, + ), + Target( + rust: 'i686-linux-android', + flutter: 'android-x86', + android: 'x86', + androidMinSdkVersion: 16, + ), + Target( + rust: 'x86_64-linux-android', + flutter: 'android-x64', + android: 'x86_64', + androidMinSdkVersion: 21, + ), + Target( + rust: 'x86_64-pc-windows-msvc', + flutter: 'windows-x64', + ), + Target( + rust: 'x86_64-unknown-linux-gnu', + flutter: 'linux-x64', + ), + Target( + rust: 'aarch64-unknown-linux-gnu', + flutter: 'linux-arm64', + ), + Target( + rust: 'x86_64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'x86_64', + ), + Target( + rust: 'aarch64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios', + darwinPlatform: 'iphoneos', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios-sim', + darwinPlatform: 'iphonesimulator', + darwinArch: 'arm64', + ), + Target( + rust: 'x86_64-apple-ios', + darwinPlatform: 'iphonesimulator', + darwinArch: 'x86_64', + ), + ]; + + static Target? forFlutterName(String flutterName) { + return all.firstWhereOrNull((element) => element.flutter == flutterName); + } + + static Target? forDarwin({ + required String platformName, + required String darwinAarch, + }) { + return all.firstWhereOrNull((element) => // + element.darwinPlatform == platformName && + element.darwinArch == darwinAarch); + } + + static Target? forRustTriple(String triple) { + return all.firstWhereOrNull((element) => element.rust == triple); + } + + static List androidTargets() { + return all + .where((element) => element.android != null) + .toList(growable: false); + } + + /// Returns buildable targets on current host platform ignoring Android targets. + static List buildableTargets() { + if (Platform.isLinux) { + // Right now we don't support cross-compiling on Linux. So we just return + // the host target. + final arch = runCommand('arch', []).stdout as String; + if (arch.trim() == 'aarch64') { + return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; + } else { + return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; + } + } + return all.where((target) { + if (Platform.isWindows) { + return target.rust.contains('-windows-'); + } else if (Platform.isMacOS) { + return target.darwinPlatform != null; + } + return false; + }).toList(growable: false); + } + + @override + String toString() { + return rust; + } + + final String? flutter; + final String rust; + final String? android; + final int? androidMinSdkVersion; + final String? darwinPlatform; + final String? darwinArch; +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/util.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/util.dart new file mode 100644 index 000000000..d8e30196d --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/util.dart @@ -0,0 +1,169 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'logging.dart'; +import 'rustup.dart'; + +final log = Logger("process"); + +class CommandFailedException implements Exception { + final String executable; + final List arguments; + final ProcessResult result; + + CommandFailedException({ + required this.executable, + required this.arguments, + required this.result, + }); + + @override + String toString() { + final stdout = result.stdout.toString().trim(); + final stderr = result.stderr.toString().trim(); + return [ + "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", + "Returned Exit Code: ${result.exitCode}", + kSeparator, + "STDOUT:", + if (stdout.isNotEmpty) stdout, + kSeparator, + "STDERR:", + if (stderr.isNotEmpty) stderr, + ].join('\n'); + } +} + +class TestRunCommandArgs { + final String executable; + final List arguments; + final String? workingDirectory; + final Map? environment; + final bool includeParentEnvironment; + final bool runInShell; + final Encoding? stdoutEncoding; + final Encoding? stderrEncoding; + + TestRunCommandArgs({ + required this.executable, + required this.arguments, + this.workingDirectory, + this.environment, + this.includeParentEnvironment = true, + this.runInShell = false, + this.stdoutEncoding, + this.stderrEncoding, + }); +} + +class TestRunCommandResult { + TestRunCommandResult({ + this.pid = 1, + this.exitCode = 0, + this.stdout = '', + this.stderr = '', + }); + + final int pid; + final int exitCode; + final String stdout; + final String stderr; +} + +TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; + +ProcessResult runCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, +}) { + if (testRunCommandOverride != null) { + final result = testRunCommandOverride!(TestRunCommandArgs( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + )); + return ProcessResult( + result.pid, + result.exitCode, + result.stdout, + result.stderr, + ); + } + log.finer('Running command $executable ${arguments.join(' ')}'); + final res = Process.runSync( + _resolveExecutable(executable), + arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + ); + if (res.exitCode != 0) { + throw CommandFailedException( + executable: executable, + arguments: arguments, + result: res, + ); + } else { + return res; + } +} + +class RustupNotFoundException implements Exception { + @override + String toString() { + return [ + ' ', + 'rustup not found in PATH.', + ' ', + 'Maybe you need to install Rust? It only takes a minute:', + ' ', + if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', + if (hasHomebrewRustInPath()) ...[ + '\$ brew unlink rust # Unlink homebrew Rust from PATH', + ], + if (!Platform.isWindows) + "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", + ' ', + ].join('\n'); + } + + static bool hasHomebrewRustInPath() { + if (!Platform.isMacOS) { + return false; + } + final envPath = Platform.environment['PATH'] ?? ''; + final paths = envPath.split(':'); + return paths.any((p) { + return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); + }); + } +} + +String _resolveExecutable(String executable) { + if (executable == 'rustup') { + final resolved = Rustup.executablePath(); + if (resolved != null) { + return resolved; + } + throw RustupNotFoundException(); + } else { + return executable; + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/integration_tests/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart new file mode 100644 index 000000000..0094c644a --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart @@ -0,0 +1,81 @@ +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; + +import 'artifacts_provider.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; + +class VerifyBinaries { + VerifyBinaries({ + required this.manifestDir, + }); + + final String manifestDir; + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final config = CargokitCrateOptions.load(manifestDir: manifestDir); + final precompiledBinaries = config.precompiledBinaries; + if (precompiledBinaries == null) { + stdout.writeln('Crate does not support precompiled binaries.'); + } else { + final crateHash = CrateHash.compute(manifestDir); + stdout.writeln('Crate hash: $crateHash'); + + for (final target in Target.all) { + final message = 'Checking ${target.rust}...'; + stdout.write(message.padRight(40)); + stdout.flush(); + + final artifacts = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + final prefix = precompiledBinaries.uriPrefix; + + bool ok = true; + + for (final artifact in artifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = + Uri.parse('$prefix$crateHash/$signatureFileName'); + + final signature = await get(signatureUrl); + if (signature.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + final asset = await get(url); + if (asset.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + + if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, + signature.bodyBytes)) { + stdout.writeln('INVALID SIGNATURE'); + ok = false; + } + } + + if (ok) { + stdout.writeln('OK'); + } + } + } + } +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/pubspec.lock b/integration_tests/rust_builder/cargokit/build_tool/pubspec.lock new file mode 100644 index 000000000..343bdd369 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/pubspec.lock @@ -0,0 +1,453 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: "direct main" + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: "direct main" + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + ed25519_edwards: + dependency: "direct main" + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + github: + dependency: "direct main" + description: + name: github + sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" + url: "https://pub.dev" + source: hosted + version: "9.17.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: "direct main" + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + toml: + dependency: "direct main" + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + version: + dependency: "direct main" + description: + name: version + sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + url: "https://pub.dev" + source: hosted + version: "11.9.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/integration_tests/rust_builder/cargokit/build_tool/pubspec.yaml b/integration_tests/rust_builder/cargokit/build_tool/pubspec.yaml new file mode 100644 index 000000000..e01aa0ae6 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/pubspec.yaml @@ -0,0 +1,30 @@ +name: build_tool +description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. +publish_to: none +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + +# Add regular dependencies here. +dependencies: + # these are pinned on purpose because the bundle_tool_runner doesn't have + # pubspec.lock. See run_build_tool.sh + logging: 1.2.0 + path: 1.8.0 + version: 3.0.0 + collection: 1.18.0 + ed25519_edwards: 0.3.1 + hex: 0.2.0 + yaml: 3.1.2 + source_span: 1.10.0 + github: 9.17.0 + args: 2.4.2 + crypto: 3.0.3 + convert: 3.1.1 + http: 1.1.0 + toml: 0.14.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/integration_tests/rust_builder/cargokit/build_tool/test/builder_test.dart b/integration_tests/rust_builder/cargokit/build_tool/test/builder_test.dart new file mode 100644 index 000000000..e92852e5f --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/test/builder_test.dart @@ -0,0 +1,28 @@ +import 'package:build_tool/src/builder.dart'; +import 'package:test/test.dart'; + +void main() { + test('parseBuildConfiguration', () { + var b = BuildEnvironment.parseBuildConfiguration('debug'); + expect(b, BuildConfiguration.debug); + + b = BuildEnvironment.parseBuildConfiguration('profile'); + expect(b, BuildConfiguration.profile); + + b = BuildEnvironment.parseBuildConfiguration('release'); + expect(b, BuildConfiguration.release); + + b = BuildEnvironment.parseBuildConfiguration('debug-dev'); + expect(b, BuildConfiguration.debug); + + b = BuildEnvironment.parseBuildConfiguration('profile'); + expect(b, BuildConfiguration.profile); + + b = BuildEnvironment.parseBuildConfiguration('profile-prod'); + expect(b, BuildConfiguration.profile); + + // fallback to release + b = BuildEnvironment.parseBuildConfiguration('unknown'); + expect(b, BuildConfiguration.release); + }); +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/test/cargo_test.dart b/integration_tests/rust_builder/cargokit/build_tool/test/cargo_test.dart new file mode 100644 index 000000000..00afe29fb --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/test/cargo_test.dart @@ -0,0 +1,28 @@ +import 'package:build_tool/src/cargo.dart'; +import 'package:test/test.dart'; + +final _cargoToml = """ +[workspace] + +[profile.release] +lto = true +panic = "abort" +opt-level = "z" +# strip = "symbols" + +[package] +name = "super_native_extensions" +version = "0.1.0" +edition = "2021" +resolver = "2" + +[lib] +crate-type = ["cdylib", "staticlib"] +"""; + +void main() { + test('parseCargoToml', () { + final info = CrateInfo.parseManifest(_cargoToml); + expect(info.packageName, 'super_native_extensions'); + }); +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/test/options_test.dart b/integration_tests/rust_builder/cargokit/build_tool/test/options_test.dart new file mode 100644 index 000000000..25a85b6a7 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/test/options_test.dart @@ -0,0 +1,75 @@ +import 'package:build_tool/src/builder.dart'; +import 'package:build_tool/src/options.dart'; +import 'package:hex/hex.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + test('parseCargoBuildOptions', () { + final yaml = """ +toolchain: nightly +extra_flags: + - -Z + # Comment here + - build-std=panic_abort,std +"""; + final node = loadYamlNode(yaml); + final options = CargoBuildOptions.parse(node); + expect(options.toolchain, Toolchain.nightly); + expect(options.flags, ['-Z', 'build-std=panic_abort,std']); + }); + + test('parsePrecompiledBinaries', () { + final yaml = """ +url_prefix: https://url-prefix +public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445 +"""; + final precompiledBinaries = PrecompiledBinaries.parse(loadYamlNode(yaml)); + final key = HEX.decode( + 'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445'); + expect(precompiledBinaries.uriPrefix, 'https://url-prefix'); + expect(precompiledBinaries.publicKey.bytes, key); + }); + + test('parseCargokitOptions', () { + const yaml = ''' +cargo: + # For smalles binaries rebuilt the standard library with panic=abort + debug: + toolchain: nightly + extra_flags: + - -Z + # Comment here + - build-std=panic_abort,std + release: + toolchain: beta + +precompiled_binaries: + url_prefix: https://url-prefix + public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445 +'''; + final options = CargokitCrateOptions.parse(loadYamlNode(yaml)); + expect(options.precompiledBinaries?.uriPrefix, 'https://url-prefix'); + final key = HEX.decode( + 'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445'); + expect(options.precompiledBinaries?.publicKey.bytes, key); + + final debugOptions = options.cargo[BuildConfiguration.debug]!; + expect(debugOptions.toolchain, Toolchain.nightly); + expect(debugOptions.flags, ['-Z', 'build-std=panic_abort,std']); + + final releaseOptions = options.cargo[BuildConfiguration.release]!; + expect(releaseOptions.toolchain, Toolchain.beta); + expect(releaseOptions.flags, []); + }); + + test('parseCargokitUserOptions', () { + const yaml = ''' +use_precompiled_binaries: false +verbose_logging: true +'''; + final options = CargokitUserOptions.parse(loadYamlNode(yaml)); + expect(options.usePrecompiledBinaries, false); + expect(options.verboseLogging, true); + }); +} diff --git a/integration_tests/rust_builder/cargokit/build_tool/test/rustup_test.dart b/integration_tests/rust_builder/cargokit/build_tool/test/rustup_test.dart new file mode 100644 index 000000000..af95303c3 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/build_tool/test/rustup_test.dart @@ -0,0 +1,66 @@ +import 'package:build_tool/src/rustup.dart'; +import 'package:build_tool/src/util.dart'; +import 'package:test/test.dart'; + +void main() { + test('rustup with no toolchains', () { + bool didListToolchains = false; + bool didInstallStable = false; + bool didListTargets = false; + testRunCommandOverride = (args) { + expect(args.executable, 'rustup'); + switch (args.arguments) { + case ['toolchain', 'list']: + didListToolchains = true; + return TestRunCommandResult(stdout: 'no installed toolchains\n'); + case ['toolchain', 'install', 'stable']: + didInstallStable = true; + return TestRunCommandResult(); + case ['target', 'list', '--toolchain', 'stable', '--installed']: + didListTargets = true; + return TestRunCommandResult( + stdout: 'x86_64-unknown-linux-gnu\nx86_64-apple-darwin\n'); + default: + throw Exception('Unexpected call: ${args.arguments}'); + } + }; + final rustup = Rustup(); + rustup.installToolchain('stable'); + expect(didInstallStable, true); + expect(didListToolchains, true); + expect(didListTargets, true); + expect(rustup.installedTargets('stable'), [ + 'x86_64-unknown-linux-gnu', + 'x86_64-apple-darwin', + ]); + testRunCommandOverride = null; + }); + + test('rustup with esp toolchain', () { + final targetsQueried = []; + testRunCommandOverride = (args) { + expect(args.executable, 'rustup'); + switch (args.arguments) { + case ['toolchain', 'list']: + return TestRunCommandResult( + stdout: 'stable-aarch64-apple-darwin (default)\n' + 'nightly-aarch64-apple-darwin\n' + 'esp\n'); + case ['target', 'list', '--toolchain', String toolchain, '--installed']: + targetsQueried.add(toolchain); + return TestRunCommandResult(stdout: '$toolchain:target\n'); + default: + throw Exception('Unexpected call: ${args.arguments}'); + } + }; + final rustup = Rustup(); + expect(targetsQueried, [ + 'stable-aarch64-apple-darwin', + 'nightly-aarch64-apple-darwin', + ]); + expect(rustup.installedTargets('stable'), + ['stable-aarch64-apple-darwin:target']); + expect(rustup.installedTargets('nightly'), + ['nightly-aarch64-apple-darwin:target']); + }); +} diff --git a/integration_tests/rust_builder/cargokit/cmake/cargokit.cmake b/integration_tests/rust_builder/cargokit/cmake/cargokit.cmake new file mode 100644 index 000000000..ddd05df9b --- /dev/null +++ b/integration_tests/rust_builder/cargokit/cmake/cargokit.cmake @@ -0,0 +1,99 @@ +SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) + +if(WIN32) + # REALPATH does not properly resolve symlinks on windows :-/ + execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +# Arguments +# - target: CMAKE target to which rust library is linked +# - manifest_dir: relative path from current folder to directory containing cargo manifest +# - lib_name: cargo package name +# - any_symbol_name: name of any exported symbol from the library. +# used on windows to force linking with library. +function(apply_cargokit target manifest_dir lib_name any_symbol_name) + + set(CARGOKIT_LIB_NAME "${lib_name}") + set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") + if (CMAKE_CONFIGURATION_TYPES) + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") + else() + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") + endif() + set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") + + if (FLUTTER_TARGET_PLATFORM) + set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") + else() + set(CARGOKIT_TARGET_PLATFORM "windows-x64") + endif() + + set(CARGOKIT_ENV + "CARGOKIT_CMAKE=${CMAKE_COMMAND}" + "CARGOKIT_CONFIGURATION=$" + "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" + "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" + "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" + "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" + "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" + "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" + ) + + if (WIN32) + set(SCRIPT_EXTENSION ".cmd") + set(IMPORT_LIB_EXTENSION ".lib") + else() + set(SCRIPT_EXTENSION ".sh") + set(IMPORT_LIB_EXTENSION "") + execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") + endif() + + # Using generators in custom command is only supported in CMake 3.20+ + if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") + foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endforeach() + else() + add_custom_command( + OUTPUT + ${OUTPUT_LIB} + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endif() + + + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) + + if (TARGET ${target}) + # If we have actual cmake target provided create target and make existing + # target depend on it + add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) + add_dependencies("${target}" "${target}_cargokit") + target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") + if(WIN32) + target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") + endif() + else() + # Otherwise (FFI) just use ALL to force building always + add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) + endif() + + # Allow adding the output library to plugin bundled libraries + set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) + +endfunction() diff --git a/integration_tests/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/integration_tests/rust_builder/cargokit/cmake/resolve_symlinks.ps1 new file mode 100644 index 000000000..3d10d283c --- /dev/null +++ b/integration_tests/rust_builder/cargokit/cmake/resolve_symlinks.ps1 @@ -0,0 +1,27 @@ +function Resolve-Symlinks { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string] $Path + ) + + [string] $separator = '/' + [string[]] $parts = $Path.Split($separator) + + [string] $realPath = '' + foreach ($part in $parts) { + if ($realPath -and !$realPath.EndsWith($separator)) { + $realPath += $separator + } + $realPath += $part + $item = Get-Item $realPath + if ($item.Target) { + $realPath = $item.Target.Replace('\', '/') + } + } + $realPath +} + +$path=Resolve-Symlinks -Path $args[0] +Write-Host $path diff --git a/integration_tests/rust_builder/cargokit/docs/architecture.md b/integration_tests/rust_builder/cargokit/docs/architecture.md new file mode 100644 index 000000000..d9bcf4e29 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/docs/architecture.md @@ -0,0 +1,104 @@ +# Cargokit Architecture + +Note: This is mostly relevant for plugins authors that want to see a bit under the hood rather then just following a tutorial. + +In ideal conditions the end-developer using the plugin should not even be aware of Cargokit existence. + +## Integration + +Cargokit is meant to be included in Flutter plugin (or application) that contains the Rust crate to be built during the Flutter build process. + +Cargokit can be either incuded as git submodule or git subtree (required for plugins - as pub does not support submodules for git dependencies). + +For a step by step tutorial on integrating Cargokit with a Flutter plugin see https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/. + +## build_tool + +Build tool is the core of cargokit. It is a Dart command line package that facilitates the build of Rust crate. It is invoked during the Flutter build process to build (or download) Rust artifacts, but it can be also used as a standalone tool. + +It handles the following commands: + +### build-cmake + +This is invoked from `cargokit.cmake` and it is used to build the Rust crate into a dynamic library on Linux and Windows (which use CMake as build system). + +The command takes no additional arguments, everything is controlled during environment variables set by `cargokit.cmake`. + +### build-gradle + +This is invoked from `plugin.gradle` and it is used to build the Rust crate into a dynamic library on Android. The command takes no additional arguments, everything is controlled during environment variables set by `plugin.gradle`. + +The build_tool installs NDK if needed, configures the Rust environment for cross compilation and then invokes `cargo build` with appropriate arguments and environment variables. + +The build-tool also acts a linker driver. + +### build-pod + +This is invoked from plugin's podspec `script_phase` through `build_pod.sh`. Bundle tool will build the Rust crate into a static library that gets linked into the plugin Framework. In this case must have `:execution_position` set to `:before_compile`. + +Cargokit will build binaries for all active architectures from XCode build and lipo them togherer. + +When using Cargokit to integrate Rust code with an application (not a plugin) you can also configure the `Cargo.toml` to just build a dynamic library. When Cargokit finds that the crate only built a dylib and no static lib, it will attempt to replace the Cocoapod framework binary with the dylib. In this case the script `:execution_position` must be set to `:after_compile`. This is *not* recommended for plugins and it's quite experimental. + +### gen-key, precompile-binaries, verify-binaries + +These are used as when providing precompiled binaries for Plugin. See [precompiled_binaries.md](precompiled_binaries.md) for more information. + +## Launching the build_tool during build. + +During Flutter build, the build tool can not be launched directly using `dart run`. Rather it is launched through `run_build_tool.sh` and `run_build_tool.cmd`. Because the `build_tool` is shipped as part of plugin, we generally don't want to write into the plugin directory during build, which would happen if the `build_tool` was simply invoked through `dart run` (For example the `.dart_tool/package_config.json` file would get written inside the `build_tool` directory). + +Instead the `run_build_tool` script creates a minimal Dart command line package in the build directory and references the `build_tool` as package. That way the `.dart_tool/package_config.json` file is created in the temporary build folder and not in the plugin itself. The script also precompiles the Dart code to speed up subsequent invocations. + +## Configuring Cargokit + +### Configuration for the Rust crate + +Cargokit can be configured through a `cargokit.yaml` file, which can be used to control the build of the Rust package and is placed into the Rust crate next to `Cargo.toml`. + +Here is an example `cargokit.yaml` with comments: +```yaml +cargo: + debug: # Configuration of cargo execution during debug builds + toolchain: stable # default + release: # Configuration of cargo execution for release builds + toolchain: nightly # rustup will be invoked with nightly toolchain + extra_flags: # extra arguments passed to cargo build + - -Z + - build-std=panic_abort,std + +# If crate ships with precompiled binaries, they can be configured here. +precompiled_binaries: + # Uri prefix used when downloading precompiled binaries. + url_prefix: https://github.com/superlistapp/super_native_extensions/releases/download/precompiled_ + + # Public key for verifying downloaded precompiled binaries. + public_key: 3a257ef1c7d72d84225ac4658d24812ada50a7a7a8a2138c2a91353389fdc514 +``` + +### Configuration for the application consuming the plugin + +A `cargokit_options.yaml` file can also be placed by developer using plugin to the root of the application package. In which case the file can be used to specify following options: + +```yaml +# Enables verbose logging of Cargokit during build +verbose_logging: true + +# Opts out of using precompiled binaries. If crate has configured +# and deployed precompiled binaries, these will be by default used whenever Rustup +# is not installed. With `use_precompiled_binaries` set to false, the build will +# instead be aborted prompting user to install Rustup. +use_precompiled_binaries: false +``` + +## Detecting Rustup + +When the plugin doesn't come with precompiled libraries (or user opt-out), `build_tool` will need to invoke Rustup during build to ensure that required Rust targets and toolchain are installed for current build and to build the Rust crate. + +Cargokit will attempt to detect Rustup in the default Rustup installation location (`~/.cargo/rustup`) as well as in PATH. This is done so that if user install Rustup but doesn't properly configure PATH, Cargokit will still work. + +If `build_tool` doesn't find Rustup, it will about the build with a message showing instructions to install Rustup specific to current platform. + +On macOS it will also detect a homebrew Rust installation in PATH and will prompt user to call `brew unlink rust` first to remove homebrew Rust installation from PATH, because it may interfere with Rustup. + +Homebrew Rust installation can not be used by Cargokit, because it can only build for host platform. Cargokit needs to be able to cross compile the Rust crate for iOS and Android and thus needs full Rustup installation. diff --git a/integration_tests/rust_builder/cargokit/docs/precompiled_binaries.md b/integration_tests/rust_builder/cargokit/docs/precompiled_binaries.md new file mode 100644 index 000000000..2026e8677 --- /dev/null +++ b/integration_tests/rust_builder/cargokit/docs/precompiled_binaries.md @@ -0,0 +1,95 @@ +# Precompiled Binaries + +Because Cargokit builds the Rust crate during Flutter build, it is inherently +dependend on the Rust toolchain being installed on the developer's machine. + +To decrease the friction, it is possible for Cargokit to use precompiled binaries instead. + +This is how the process of using precompiled binaries looks from the perspective of the build on developer machine: + +1. Cargokit checks if there is `cargokit_options.yaml` file in the root folder of target application. If there is one, it will be checked for `use_precompiled_binaries` options to see if user opted out of using precompiled binaries. In which case Cargokit will insist on building from source. Cargokit will also build from source if the configuration file is absent, but user has Rustup installed. + +2. Cargokit checks if there is `cargokit.yaml` file placed in the Rust crate. If there is one, it will be checked for `precompiled_binaries` section to see if crate supports precompiled binaries. The configuration section must contain a public key and URL prefix. + +3. Cargokit computes a `crate-hash`. This is a SHA256 hash value computed from all Rust files inside crate, `Cargo.toml`, `Cargo.lock` and `cargokit.yaml`. This uniquely identifies the crate and it is used to find the correct precompiled binaries. + +4. Cargokit will attempt to download the precompiled binaries for target platform and `crate_hash` combination and a signature file for each downloaded binary. If download succeeds, the binary content will be verified against the signature and public key included in `cargokit.yaml` (which is part of Rust crate and thus part of published Flutter package). + +5. If the verification succeeds, the precompiled binaries will be used. Otherwise the binary will be discarded and Cargokit will insist on building from source. + +## Providing precompiled binaries + +Note that this assumes that precompiled binaries will be generated during github actions and deployed as github releases. + +### Use `build_tool` to generate a key-pair: + +``` +dart run build_tool gen-key +``` + +This will print the private key and public key. Store the private key securely. It needs to be provided as a secret to github action. + +The public key should be included in `cargokit.yaml` file in the Rust crate. + +### Provide a `cargokit.yaml` file in the Rust crate + +The file must be placed alongside Cargo.toml. + +```yaml +precompiled_binaries: + # Uri prefix used when downloading precompiled binaries. + url_prefix: https://github.com///releases/download/precompiled_ + + # Public key for verifying downloaded precompiled binaries. + public_key: +``` + +### Configure a github action to build and upload precompiled binaries. + +The github action should be run at every commit to main branch (and possibly other branches). + +The action needs two secrets - private key for signing binaries and GitHub token for uploading binaries as releases. Here is example action that precompiles and uploads binaries for all supported targets. + +```yaml +on: + push: + branches: [ main ] + +name: Precompile Binaries + +jobs: + Precompile: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macOS-latest + - windows-latest + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v1 + - name: Install GTK + if: (matrix.os == 'ubuntu-latest') + run: sudo apt-get update && sudo apt-get install libgtk-3-dev + - name: Precompile + if: (matrix.os == 'macOS-latest') || (matrix.os == 'windows-latest') + run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions + working-directory: super_native_extensions/cargokit/build_tool + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} + PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }} + - name: Precompile (with Android) + if: (matrix.os == 'ubuntu-latest') + run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions --android-sdk-location=/usr/local/lib/android/sdk --android-ndk-version=24.0.8215888 --android-min-sdk-version=23 + working-directory: super_native_extensions/cargokit/build_tool + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} + PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }} +``` + +By default the `built_tool precompile-binaries` commands build and uploads the binaries for all targets buildable from current host. This can be overriden using the `--target ` argument. + +Android binaries will be built when `--android-sdk-location` and `--android-ndk-version` arguments are provided. + diff --git a/integration_tests/rust_builder/cargokit/gradle/plugin.gradle b/integration_tests/rust_builder/cargokit/gradle/plugin.gradle new file mode 100644 index 000000000..37dd086af --- /dev/null +++ b/integration_tests/rust_builder/cargokit/gradle/plugin.gradle @@ -0,0 +1,176 @@ +import java.nio.file.Paths +import org.apache.tools.ant.taskdefs.condition.Os + +CargoKitPlugin.file = buildscript.sourceFile + +apply plugin: CargoKitPlugin + +class CargoKitExtension { + String manifestDir; // Relative path to folder containing Cargo.toml + String libname; // Library name within Cargo.toml. Must be a cdylib +} + +abstract class CargoKitBuildTask extends DefaultTask { + + @Input + String buildMode + + @Input + String buildDir + + @Input + String outputDir + + @Input + String ndkVersion + + @Input + String sdkDirectory + + @Input + int compileSdkVersion; + + @Input + int minSdkVersion; + + @Input + String pluginFile + + @Input + List targetPlatforms + + @TaskAction + def build() { + if (project.cargokit.manifestDir == null) { + throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); + } + + if (project.cargokit.libname == null) { + throw new GradleException("Property 'libname' must be set on cargokit extension"); + } + + def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" + def path = Paths.get(new File(pluginFile).parent, "..", executableName); + + def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) + + def rootProjectDir = project.rootProject.projectDir + + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { + commandLine 'chmod', '+x', path + } + } + + project.exec { + executable path + args "build-gradle" + environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir + environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" + environment "CARGOKIT_MANIFEST_DIR", manifestDir + environment "CARGOKIT_CONFIGURATION", buildMode + environment "CARGOKIT_TARGET_TEMP_DIR", buildDir + environment "CARGOKIT_OUTPUT_DIR", outputDir + environment "CARGOKIT_NDK_VERSION", ndkVersion + environment "CARGOKIT_SDK_DIR", sdkDirectory + environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion + environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion + environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") + environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] + } + } +} + +class CargoKitPlugin implements Plugin { + + static String file; + + private Plugin findFlutterPlugin(Project rootProject) { + _findFlutterPlugin(rootProject.childProjects) + } + + private Plugin _findFlutterPlugin(Map projects) { + for (project in projects) { + for (plugin in project.value.getPlugins()) { + if (plugin.class.name == "FlutterPlugin") { + return plugin; + } + } + def plugin = _findFlutterPlugin(project.value.childProjects); + if (plugin != null) { + return plugin; + } + } + return null; + } + + @Override + void apply(Project project) { + def plugin = findFlutterPlugin(project.rootProject); + + project.extensions.create("cargokit", CargoKitExtension) + + if (plugin == null) { + print("Flutter plugin not found, CargoKit plugin will not be applied.") + return; + } + + def cargoBuildDir = "${project.buildDir}/build" + + // Determine if the project is an application or library + def isApplication = plugin.project.plugins.hasPlugin('com.android.application') + def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants + + variants.all { variant -> + + final buildType = variant.buildType.name + + def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; + def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; + jniLibs.srcDir(new File(cargoOutputDir)) + + def platforms = plugin.getTargetPlatforms().collect() + + // Same thing addFlutterDependencies does in flutter.gradle + if (buildType == "debug") { + platforms.add("android-x86") + platforms.add("android-x64") + } + + // The task name depends on plugin properties, which are not available + // at this point + project.getGradle().afterProject { + def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; + + if (project.tasks.findByName(taskName)) { + return + } + + if (plugin.project.android.ndkVersion == null) { + throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") + } + + def task = project.tasks.create(taskName, CargoKitBuildTask.class) { + buildMode = variant.buildType.name + buildDir = cargoBuildDir + outputDir = cargoOutputDir + ndkVersion = plugin.project.android.ndkVersion + sdkDirectory = plugin.project.android.sdkDirectory + minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int + compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int + targetPlatforms = platforms + pluginFile = CargoKitPlugin.file + } + def onTask = { newTask -> + if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { + newTask.dependsOn task + // Fix gradle 7.4.2 not picking up JNI library changes + newTask.outputs.upToDateWhen { false } + } + } + project.tasks.each onTask + project.tasks.whenTaskAdded onTask + } + } + } +} diff --git a/integration_tests/rust_builder/cargokit/run_build_tool.cmd b/integration_tests/rust_builder/cargokit/run_build_tool.cmd new file mode 100644 index 000000000..c45d0aa8b --- /dev/null +++ b/integration_tests/rust_builder/cargokit/run_build_tool.cmd @@ -0,0 +1,91 @@ +@echo off +setlocal + +setlocal ENABLEDELAYEDEXPANSION + +SET BASEDIR=%~dp0 + +if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( + mkdir "%CARGOKIT_TOOL_TEMP_DIR%" +) +cd /D "%CARGOKIT_TOOL_TEMP_DIR%" + +SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool +SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart + +set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% + +( + echo name: build_tool_runner + echo version: 1.0.0 + echo publish_to: none + echo. + echo environment: + echo sdk: '^>=3.0.0 ^<4.0.0' + echo. + echo dependencies: + echo build_tool: + echo path: %BUILD_TOOL_PKG_DIR_POSIX% +) >pubspec.yaml + +if not exist bin ( + mkdir bin +) + +( + echo import 'package:build_tool/build_tool.dart' as build_tool; + echo void main^(List^ args^) ^{ + echo build_tool.runMain^(args^); + echo ^} +) >bin\build_tool_runner.dart + +SET PRECOMPILED=bin\build_tool_runner.dill + +REM To detect changes in package we compare output of DIR /s (recursive) +set PREV_PACKAGE_INFO=.dart_tool\package_info.prev +set CUR_PACKAGE_INFO=.dart_tool\package_info.cur + +DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" + +REM Last line in dir output is free space on harddrive. That is bound to +REM change between invocation so we need to remove it +( + Set "Line=" + For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( + SetLocal EnableDelayedExpansion + If Defined Line Echo !Line! + EndLocal + Set "Line=%%A") +) >"%CUR_PACKAGE_INFO%" +DEL "%CUR_PACKAGE_INFO%_orig" + +REM Compare current directory listing with previous +FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 + +If %ERRORLEVEL% neq 0 ( + REM Changed - copy current to previous and remove precompiled kernel + if exist "%PREV_PACKAGE_INFO%" ( + DEL "%PREV_PACKAGE_INFO%" + ) + MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" + if exist "%PRECOMPILED%" ( + DEL "%PRECOMPILED%" + ) +) + +REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% +REM which means we need to do pub get and precompile +if not exist "%PRECOMPILED%" ( + echo Running pub get in "%cd%" + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart +) + +"%DART%" "%PRECOMPILED%" %* + +REM 253 means invalid snapshot version. +If %ERRORLEVEL% equ 253 ( + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart + "%DART%" "%PRECOMPILED%" %* +) diff --git a/integration_tests/rust_builder/cargokit/run_build_tool.sh b/integration_tests/rust_builder/cargokit/run_build_tool.sh new file mode 100755 index 000000000..6e594a23d --- /dev/null +++ b/integration_tests/rust_builder/cargokit/run_build_tool.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$(dirname "$0") + +mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" + +cd "$CARGOKIT_TOOL_TEMP_DIR" + +# Write a very simple bin package in temp folder that depends on build_tool package +# from Cargokit. This is done to ensure that we don't pollute Cargokit folder +# with .dart_tool contents. + +BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" + +if [[ -z $FLUTTER_ROOT ]]; then # not defined + DART=dart +else + DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" +fi + +cat << EOF > "pubspec.yaml" +name: build_tool_runner +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + build_tool: + path: "$BUILD_TOOL_PKG_DIR" +EOF + +mkdir -p "bin" + +cat << EOF > "bin/build_tool_runner.dart" +import 'package:build_tool/build_tool.dart' as build_tool; +void main(List args) { + build_tool.runMain(args); +} +EOF + +# Create alias for `shasum` if it does not exist and `sha1sum` exists +if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then + shopt -s expand_aliases + alias shasum="sha1sum" +fi + +# Dart run will not cache any package that has a path dependency, which +# is the case for our build_tool_runner. So instead we precompile the package +# ourselves. +# To invalidate the cached kernel we use the hash of ls -LR of the build_tool +# package directory. This should be good enough, as the build_tool package +# itself is not meant to have any path dependencies. + +if [[ "$OSTYPE" == "darwin"* ]]; then + PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) +else + PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) +fi + +PACKAGE_HASH_FILE=".package_hash" + +if [ -f "$PACKAGE_HASH_FILE" ]; then + EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") + if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then + rm "$PACKAGE_HASH_FILE" + fi +fi + +# Run pub get if needed. +if [ ! -f "$PACKAGE_HASH_FILE" ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" +fi + +set +e + +"$DART" bin/build_tool_runner.dill "$@" + +exit_code=$? + +# 253 means invalid snapshot version. +if [ $exit_code == 253 ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + "$DART" bin/build_tool_runner.dill "$@" + exit_code=$? +fi + +exit $exit_code diff --git a/integration_tests/rust_builder/ios/Classes/dummy_file.c b/integration_tests/rust_builder/ios/Classes/dummy_file.c new file mode 100644 index 000000000..e06dab996 --- /dev/null +++ b/integration_tests/rust_builder/ios/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/integration_tests/rust_builder/ios/rust_native_api_tests.podspec b/integration_tests/rust_builder/ios/rust_native_api_tests.podspec new file mode 100644 index 000000000..a62354510 --- /dev/null +++ b/integration_tests/rust_builder/ios/rust_native_api_tests.podspec @@ -0,0 +1,45 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint example_app.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'rust_native_api_tests' + s.version = '0.0.1' + s.summary = 'Rust tests for WebF Native APIs' + s.description = <<-DESC +Rust tests for WebF Native APIs + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust rust_native_api_tests', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/librust_native_api_tests.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_native_api_tests.a', + } +end diff --git a/integration_tests/rust_builder/linux/CMakeLists.txt b/integration_tests/rust_builder/linux/CMakeLists.txt new file mode 100644 index 000000000..afff09b63 --- /dev/null +++ b/integration_tests/rust_builder/linux/CMakeLists.txt @@ -0,0 +1,26 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "rust_native_api_tests") +project(${PROJECT_NAME} LANGUAGES CXX) + + +# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# Replace add_subdirectory that references old C++ code with Cargokit: +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../rust rust_native_api_tests "") +# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(rust_native_api_tests_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + # Defined in ../src/CMakeLists.txt. + # This can be changed to accommodate different builds. + PARENT_SCOPE +) diff --git a/integration_tests/rust_builder/macos/Classes/dummy_file.c b/integration_tests/rust_builder/macos/Classes/dummy_file.c new file mode 100644 index 000000000..e06dab996 --- /dev/null +++ b/integration_tests/rust_builder/macos/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/integration_tests/rust_builder/macos/rust_native_api_tests.podspec b/integration_tests/rust_builder/macos/rust_native_api_tests.podspec new file mode 100644 index 000000000..bf5f75af5 --- /dev/null +++ b/integration_tests/rust_builder/macos/rust_native_api_tests.podspec @@ -0,0 +1,44 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint rust_lib_my_app.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'rust_native_api_tests' + s.version = '0.0.1' + s.summary = 'Rust tests for WebF Native APIs' + s.description = <<-DESC +Rust tests for WebF Native APIs + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust rust_native_api_tests', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/librust_native_api_tests.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_native_api_tests.a', + } +end diff --git a/integration_tests/rust_builder/pubspec.yaml b/integration_tests/rust_builder/pubspec.yaml new file mode 100644 index 000000000..f1be2a402 --- /dev/null +++ b/integration_tests/rust_builder/pubspec.yaml @@ -0,0 +1,49 @@ +name: rust_native_api_tests +description: "Rust tests for WebF Native APIs" +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + ffi: ^2.1.0 + ffigen: ^9.0.1 + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + # + # Please refer to README.md for a detailed explanation. + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true diff --git a/integration_tests/rust_builder/rust/.gitignore b/integration_tests/rust_builder/rust/.gitignore new file mode 100644 index 000000000..58db1f31e --- /dev/null +++ b/integration_tests/rust_builder/rust/.gitignore @@ -0,0 +1,3 @@ +target +.idea +Cargo.lock diff --git a/integration_tests/rust_builder/rust/Cargo.toml b/integration_tests/rust_builder/rust/Cargo.toml new file mode 100644 index 000000000..5383c5941 --- /dev/null +++ b/integration_tests/rust_builder/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rust_native_api_tests" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +webf-sys = "0.16.0" + +[patch.crates-io] +webf-sys = { path = "../../../bridge/rusty_webf_sys" } diff --git a/integration_tests/rust_builder/rust/src/lib.rs b/integration_tests/rust_builder/rust/src/lib.rs new file mode 100644 index 000000000..a56563eac --- /dev/null +++ b/integration_tests/rust_builder/rust/src/lib.rs @@ -0,0 +1,156 @@ +use std::cell::RefCell; +use std::ffi::c_void; +use std::fs::{self, File}; +use std::io::Write; +use std::rc::Rc; +use webf_sys::event::Event; +use webf_sys::executing_context::ExecutingContextRustMethods; +use webf_sys::webf_future::FutureRuntime; +use webf_sys::{document, initialize_webf_api, AddEventListenerOptions, ElementMethods, EventTargetMethods, RustValue}; +use webf_sys::element::Element; +use webf_sys::node::NodeMethods; + +#[no_mangle] +pub extern "C" fn init_webf_test_app(handle: RustValue) -> *mut c_void { + let context = initialize_webf_api(handle); + println!("Context created"); + let exception_state = context.create_exception_state(); + let document = context.document(); + let navigator = context.navigator(); + + let ua_string = navigator.user_agent(&exception_state); + println!("User Agent: {}", ua_string); + + let local_storage = context.local_storage(); + + let result = local_storage.set_item("test", "test2", &exception_state); + + match result { + Ok(_) => { + println!("Local Storage Set Item Success"); + }, + Err(err) => { + println!("Local Storage Set Item Failed: {:?}", err); + } + } + + println!("Local Storage value for \"a\": {:?}", local_storage.get_item("a", &exception_state)); + println!("Local Storage Keys: {:?}", local_storage.get_all_keys(&exception_state)); + println!("Local Storage Length: {:?}", local_storage.length(&exception_state)); + println!("Local Storage value for \"test\": {:?}", local_storage.get_item("test", &exception_state)); + + local_storage.clear(&exception_state); + + let context2 = context.clone(); + + let runtime = Rc::new(RefCell::new(FutureRuntime::new())); + + runtime.borrow_mut().spawn(async move { + let context = context2.clone(); + let exception_state = context.create_exception_state(); + let async_storage_2 = context.async_storage(); + + println!("Hello from Rust async context!"); + + async_storage_2.set_item("a", "b", &exception_state).await.unwrap(); + let result = async_storage_2.get_item("a", &exception_state).await; + + match result { + Ok(value) => { + println!("Async Storage Get Item Success: {:?}", value); + }, + Err(err) => { + println!("Async Storage Get Item Failed: {:?}", err); + } + } + }); + + let runtime_run_task_callback = Box::new(move || { + runtime.borrow_mut().run(); + }); + + context.set_run_rust_future_tasks(runtime_run_task_callback, &exception_state).unwrap(); + + let click_event = document.create_event("custom_click", &exception_state).unwrap(); + document.dispatch_event(&click_event, &exception_state); + + let div_element = document.create_element("div", &exception_state).unwrap(); + + let event_listener_options = AddEventListenerOptions { + passive: 0, + once: 0, + capture: 0, + }; + + let event_handler = Box::new(|event: &Event| { + let context = event.context(); + let exception_state = context.create_exception_state(); + let document = context.document(); + let div = document.create_element("div", &exception_state).unwrap(); + let text_node = document.create_text_node("Created By Event Handler", &exception_state).unwrap(); + div.append_child(&text_node.as_node(), &exception_state).unwrap(); + document.body().append_child(&div.as_node(), &exception_state).unwrap(); + }); + + div_element.add_event_listener("custom_click", event_handler.clone(), &event_listener_options, &exception_state).unwrap(); + + let real_click_handler = Box::new(move |event: &Event| { + match event.as_mouse_event() { + Ok(mouse_event) => { + let x = mouse_event.offset_x(); + let y = mouse_event.offset_y(); + let document = context.document(); + let exception_state = context.create_exception_state(); + let div = document.create_element("div", &exception_state).unwrap(); + let text_node = document.create_text_node(format!("Mouse Clicked at x: {}, y: {}", x, y).as_str(), &exception_state).unwrap(); + div.append_child(&text_node.as_node(), &exception_state).unwrap(); + document.body().append_child(&div.as_node(), &exception_state).unwrap(); + }, + Err(_) => { + println!("Not a mouse event"); + } + } + + let context = event.context(); + let exception_state = context.create_exception_state(); + let document = context.document(); + let custom_click_event = document.create_event("custom_click", &exception_state); + + match custom_click_event { + Ok(custom_click_event) => { + let event_target = event.target(); + let element: Element = event_target.as_element().unwrap(); + let _ = element.dispatch_event(&custom_click_event, &exception_state); + }, + Err(err) => { + println!("{err}"); + } + } + }); + + div_element.add_event_listener("click", real_click_handler, &event_listener_options, &exception_state).unwrap(); + + let text_node = document.create_text_node("From Rust", &exception_state).unwrap(); + + div_element.append_child(&text_node.as_node(), &exception_state).expect("append Node Failed"); + + document.body().append_child(&div_element.as_node(), &exception_state).unwrap(); + + let event_cleaner_element = document.create_element("button", &exception_state).unwrap(); + + let event_cleaner_text_node = document.create_text_node("Remove Event", &exception_state).unwrap(); + + event_cleaner_element.append_child(&event_cleaner_text_node.as_node(), &exception_state).unwrap(); + + let event_cleaner_handler = Box::new(move |event: &Event| { + let context = event.context(); + let exception_state = context.create_exception_state(); + + let _ = div_element.remove_event_listener("custom_click", event_handler.clone(), &exception_state); + }); + + event_cleaner_element.add_event_listener("click", event_cleaner_handler, &event_listener_options, &exception_state).unwrap(); + + document.body().append_child(&event_cleaner_element.as_node(), &exception_state).unwrap(); + std::ptr::null_mut() +} diff --git a/integration_tests/rust_builder/windows/.gitignore b/integration_tests/rust_builder/windows/.gitignore new file mode 100644 index 000000000..b3eb2be16 --- /dev/null +++ b/integration_tests/rust_builder/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/integration_tests/rust_builder/windows/CMakeLists.txt b/integration_tests/rust_builder/windows/CMakeLists.txt new file mode 100644 index 000000000..7c6d8953a --- /dev/null +++ b/integration_tests/rust_builder/windows/CMakeLists.txt @@ -0,0 +1,26 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "rust_native_api_tests") +project(${PROJECT_NAME} LANGUAGES CXX) + + +# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# Replace add_subdirectory that references old C++ code with Cargokit: +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} "../rust" rust_native_api_tests "") + +# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(rust_native_api_tests_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + # Defined in ../src/CMakeLists.txt. + # This can be changed to accommodate different builds. + PARENT_SCOPE +) diff --git a/integration_tests/windows/flutter/generated_plugins.cmake b/integration_tests/windows/flutter/generated_plugins.cmake index e3552774d..1d613595a 100644 --- a/integration_tests/windows/flutter/generated_plugins.cmake +++ b/integration_tests/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + rust_native_api_tests ) set(PLUGIN_BUNDLED_LIBRARIES) From 6d6a8f6a05495374ae40393a8c124150b6a29adb Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sat, 18 Jan 2025 18:51:03 +0800 Subject: [PATCH 4/7] feat: first rust function in integration test --- bridge/core/native/native_loader.cc | 4 +- .../rust_builder/rust/src/lib.rs | 123 +----------------- integration_tests/spec_group.json | 6 + integration_tests/specs/rust/main.ts | 6 + 4 files changed, 16 insertions(+), 123 deletions(-) create mode 100644 integration_tests/specs/rust/main.ts diff --git a/bridge/core/native/native_loader.cc b/bridge/core/native/native_loader.cc index e431f785d..f7d0eed80 100644 --- a/bridge/core/native/native_loader.cc +++ b/bridge/core/native/native_loader.cc @@ -40,6 +40,8 @@ static void ExecuteNativeLibrary(PluginLibraryEntryPoint entry_point, native_library_load_context->context->RunRustFutureTasks(); } + native_library_load_context->promise_resolver->Resolve(JS_UNDEFINED); + delete native_library_load_context; } @@ -75,4 +77,4 @@ ScriptPromise NativeLoader::loadNativeLibrary(const AtomicString& lib_name, return resolver->Promise(); } -} // namespace webf \ No newline at end of file +} // namespace webf diff --git a/integration_tests/rust_builder/rust/src/lib.rs b/integration_tests/rust_builder/rust/src/lib.rs index a56563eac..e7e93f8b4 100644 --- a/integration_tests/rust_builder/rust/src/lib.rs +++ b/integration_tests/rust_builder/rust/src/lib.rs @@ -1,21 +1,12 @@ -use std::cell::RefCell; use std::ffi::c_void; -use std::fs::{self, File}; -use std::io::Write; -use std::rc::Rc; -use webf_sys::event::Event; use webf_sys::executing_context::ExecutingContextRustMethods; -use webf_sys::webf_future::FutureRuntime; -use webf_sys::{document, initialize_webf_api, AddEventListenerOptions, ElementMethods, EventTargetMethods, RustValue}; -use webf_sys::element::Element; -use webf_sys::node::NodeMethods; +use webf_sys::{initialize_webf_api, RustValue}; #[no_mangle] pub extern "C" fn init_webf_test_app(handle: RustValue) -> *mut c_void { let context = initialize_webf_api(handle); println!("Context created"); let exception_state = context.create_exception_state(); - let document = context.document(); let navigator = context.navigator(); let ua_string = navigator.user_agent(&exception_state); @@ -40,117 +31,5 @@ pub extern "C" fn init_webf_test_app(handle: RustValue { - println!("Async Storage Get Item Success: {:?}", value); - }, - Err(err) => { - println!("Async Storage Get Item Failed: {:?}", err); - } - } - }); - - let runtime_run_task_callback = Box::new(move || { - runtime.borrow_mut().run(); - }); - - context.set_run_rust_future_tasks(runtime_run_task_callback, &exception_state).unwrap(); - - let click_event = document.create_event("custom_click", &exception_state).unwrap(); - document.dispatch_event(&click_event, &exception_state); - - let div_element = document.create_element("div", &exception_state).unwrap(); - - let event_listener_options = AddEventListenerOptions { - passive: 0, - once: 0, - capture: 0, - }; - - let event_handler = Box::new(|event: &Event| { - let context = event.context(); - let exception_state = context.create_exception_state(); - let document = context.document(); - let div = document.create_element("div", &exception_state).unwrap(); - let text_node = document.create_text_node("Created By Event Handler", &exception_state).unwrap(); - div.append_child(&text_node.as_node(), &exception_state).unwrap(); - document.body().append_child(&div.as_node(), &exception_state).unwrap(); - }); - - div_element.add_event_listener("custom_click", event_handler.clone(), &event_listener_options, &exception_state).unwrap(); - - let real_click_handler = Box::new(move |event: &Event| { - match event.as_mouse_event() { - Ok(mouse_event) => { - let x = mouse_event.offset_x(); - let y = mouse_event.offset_y(); - let document = context.document(); - let exception_state = context.create_exception_state(); - let div = document.create_element("div", &exception_state).unwrap(); - let text_node = document.create_text_node(format!("Mouse Clicked at x: {}, y: {}", x, y).as_str(), &exception_state).unwrap(); - div.append_child(&text_node.as_node(), &exception_state).unwrap(); - document.body().append_child(&div.as_node(), &exception_state).unwrap(); - }, - Err(_) => { - println!("Not a mouse event"); - } - } - - let context = event.context(); - let exception_state = context.create_exception_state(); - let document = context.document(); - let custom_click_event = document.create_event("custom_click", &exception_state); - - match custom_click_event { - Ok(custom_click_event) => { - let event_target = event.target(); - let element: Element = event_target.as_element().unwrap(); - let _ = element.dispatch_event(&custom_click_event, &exception_state); - }, - Err(err) => { - println!("{err}"); - } - } - }); - - div_element.add_event_listener("click", real_click_handler, &event_listener_options, &exception_state).unwrap(); - - let text_node = document.create_text_node("From Rust", &exception_state).unwrap(); - - div_element.append_child(&text_node.as_node(), &exception_state).expect("append Node Failed"); - - document.body().append_child(&div_element.as_node(), &exception_state).unwrap(); - - let event_cleaner_element = document.create_element("button", &exception_state).unwrap(); - - let event_cleaner_text_node = document.create_text_node("Remove Event", &exception_state).unwrap(); - - event_cleaner_element.append_child(&event_cleaner_text_node.as_node(), &exception_state).unwrap(); - - let event_cleaner_handler = Box::new(move |event: &Event| { - let context = event.context(); - let exception_state = context.create_exception_state(); - - let _ = div_element.remove_event_listener("custom_click", event_handler.clone(), &exception_state); - }); - - event_cleaner_element.add_event_listener("click", event_cleaner_handler, &event_listener_options, &exception_state).unwrap(); - - document.body().append_child(&event_cleaner_element.as_node(), &exception_state).unwrap(); std::ptr::null_mut() } diff --git a/integration_tests/spec_group.json b/integration_tests/spec_group.json index 5c573199d..892930268 100644 --- a/integration_tests/spec_group.json +++ b/integration_tests/spec_group.json @@ -117,5 +117,11 @@ "specs": [ "specs/svg/**/*.{js,jsx,ts,tsx,html,svg}" ] + }, + { + "name": "Rust", + "specs": [ + "specs/rust/**/*.{js,jsx,ts,tsx,html}" + ] } ] diff --git a/integration_tests/specs/rust/main.ts b/integration_tests/specs/rust/main.ts new file mode 100644 index 000000000..f794cc930 --- /dev/null +++ b/integration_tests/specs/rust/main.ts @@ -0,0 +1,6 @@ +describe('Rust', () => { + it('should work with rust native library', async () => { + // @ts-expect-error + await nativeLoader.loadNativeLibrary('rust_native_api_tests', {}).catch(err => console.log(err)); + }); +}); From 1f348ac1285dec286dcec7ef4be80375b3ac6fd6 Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sat, 18 Jan 2025 20:36:11 +0800 Subject: [PATCH 5/7] feat: naive test runner --- .../rust_builder/rust/src/lib.rs | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/integration_tests/rust_builder/rust/src/lib.rs b/integration_tests/rust_builder/rust/src/lib.rs index e7e93f8b4..57955e0a3 100644 --- a/integration_tests/rust_builder/rust/src/lib.rs +++ b/integration_tests/rust_builder/rust/src/lib.rs @@ -1,35 +1,41 @@ use std::ffi::c_void; use webf_sys::executing_context::ExecutingContextRustMethods; -use webf_sys::{initialize_webf_api, RustValue}; +use webf_sys::{initialize_webf_api, ExecutingContext, RustValue}; -#[no_mangle] -pub extern "C" fn init_webf_test_app(handle: RustValue) -> *mut c_void { - let context = initialize_webf_api(handle); - println!("Context created"); - let exception_state = context.create_exception_state(); +fn test_user_agent(context: ExecutingContext) { let navigator = context.navigator(); - + let exception_state = context.create_exception_state(); let ua_string = navigator.user_agent(&exception_state); - println!("User Agent: {}", ua_string); - let local_storage = context.local_storage(); + assert!(ua_string.contains("WebF")); +} - let result = local_storage.set_item("test", "test2", &exception_state); +fn test_hardware_concurrency(context: ExecutingContext) { + let navigator = context.navigator(); + let exception_state = context.create_exception_state(); + let hardware_concurrency = navigator.hardware_concurrency(&exception_state); - match result { - Ok(_) => { - println!("Local Storage Set Item Success"); - }, - Err(err) => { - println!("Local Storage Set Item Failed: {:?}", err); - } + assert!(hardware_concurrency > 0); +} + +fn webf_test_runner(tests: &[&dyn Fn(ExecutingContext)], context: ExecutingContext) { + println!("Running {} tests", tests.len()); + for test in tests { + test(context.clone()); + println!("Test passed"); } +} + +#[no_mangle] +pub extern "C" fn init_webf_test_app(handle: RustValue) -> *mut c_void { + let context = initialize_webf_api(handle); + + let tests: &[&dyn Fn(ExecutingContext)] = &[ + &test_user_agent, + &test_hardware_concurrency, + ]; - println!("Local Storage value for \"a\": {:?}", local_storage.get_item("a", &exception_state)); - println!("Local Storage Keys: {:?}", local_storage.get_all_keys(&exception_state)); - println!("Local Storage Length: {:?}", local_storage.length(&exception_state)); - println!("Local Storage value for \"test\": {:?}", local_storage.get_item("test", &exception_state)); + webf_test_runner(tests, context); - local_storage.clear(&exception_state); std::ptr::null_mut() } From cb29c7b741cd704d6e435654a20cb9636a12ab9e Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sat, 18 Jan 2025 20:46:57 +0800 Subject: [PATCH 6/7] feat: basic test file structure --- .../rust_builder/rust/src/lib.rs | 23 +++-------- .../rust_builder/rust/src/navigator/mod.rs | 1 + .../rust/src/navigator/navigator.rs | 17 ++++++++ .../rust_builder/rust/src/storage/mod.rs | 1 + .../rust_builder/rust/src/storage/set.rs | 39 +++++++++++++++++++ 5 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 integration_tests/rust_builder/rust/src/navigator/mod.rs create mode 100644 integration_tests/rust_builder/rust/src/navigator/navigator.rs create mode 100644 integration_tests/rust_builder/rust/src/storage/mod.rs create mode 100644 integration_tests/rust_builder/rust/src/storage/set.rs diff --git a/integration_tests/rust_builder/rust/src/lib.rs b/integration_tests/rust_builder/rust/src/lib.rs index 57955e0a3..f4a8a0d93 100644 --- a/integration_tests/rust_builder/rust/src/lib.rs +++ b/integration_tests/rust_builder/rust/src/lib.rs @@ -2,21 +2,8 @@ use std::ffi::c_void; use webf_sys::executing_context::ExecutingContextRustMethods; use webf_sys::{initialize_webf_api, ExecutingContext, RustValue}; -fn test_user_agent(context: ExecutingContext) { - let navigator = context.navigator(); - let exception_state = context.create_exception_state(); - let ua_string = navigator.user_agent(&exception_state); - - assert!(ua_string.contains("WebF")); -} - -fn test_hardware_concurrency(context: ExecutingContext) { - let navigator = context.navigator(); - let exception_state = context.create_exception_state(); - let hardware_concurrency = navigator.hardware_concurrency(&exception_state); - - assert!(hardware_concurrency > 0); -} +pub mod navigator; +pub mod storage; fn webf_test_runner(tests: &[&dyn Fn(ExecutingContext)], context: ExecutingContext) { println!("Running {} tests", tests.len()); @@ -31,8 +18,10 @@ pub extern "C" fn init_webf_test_app(handle: RustValue 0); +} + +pub fn test_user_agent(context: ExecutingContext) { + let navigator = context.navigator(); + let exception_state = context.create_exception_state(); + let ua_string = navigator.user_agent(&exception_state); + + assert!(ua_string.contains("WebF")); +} diff --git a/integration_tests/rust_builder/rust/src/storage/mod.rs b/integration_tests/rust_builder/rust/src/storage/mod.rs new file mode 100644 index 000000000..d7e525593 --- /dev/null +++ b/integration_tests/rust_builder/rust/src/storage/mod.rs @@ -0,0 +1 @@ +pub mod set; diff --git a/integration_tests/rust_builder/rust/src/storage/set.rs b/integration_tests/rust_builder/rust/src/storage/set.rs new file mode 100644 index 000000000..3d98eff01 --- /dev/null +++ b/integration_tests/rust_builder/rust/src/storage/set.rs @@ -0,0 +1,39 @@ +use webf_sys::ExecutingContext; + +pub fn test_local_storage_method_access(context: ExecutingContext) { + let storage = context.local_storage(); + let exception_state = context.create_exception_state(); + storage.clear(&exception_state); + + let keys = storage.get_all_keys(&exception_state); + assert_eq!(keys.len(), 0); + + storage.set_item("name", "user1", &exception_state).unwrap(); + + let keys = storage.get_all_keys(&exception_state); + assert_eq!(keys.len(), 1); + + storage.remove_item("name", &exception_state).unwrap(); + + let keys = storage.get_all_keys(&exception_state); + assert_eq!(keys.len(), 0); +} + +pub fn test_session_storage_method_access(context: ExecutingContext) { + let storage = context.session_storage(); + let exception_state = context.create_exception_state(); + storage.clear(&exception_state); + + let keys = storage.get_all_keys(&exception_state); + assert_eq!(keys.len(), 0); + + storage.set_item("name", "user1", &exception_state).unwrap(); + + let keys = storage.get_all_keys(&exception_state); + assert_eq!(keys.len(), 1); + + storage.remove_item("name", &exception_state).unwrap(); + + let keys = storage.get_all_keys(&exception_state); + assert_eq!(keys.len(), 0); +} From e875814eeab620f85f1f399e9eb5b19868cf1a59 Mon Sep 17 00:00:00 2001 From: LeuisKen Date: Sat, 18 Jan 2025 23:05:57 +0800 Subject: [PATCH 7/7] feat: enable async test cases --- bridge/core/executing_context.cc | 2 + bridge/core/native/native_loader.cc | 2 - .../rust/src/async_storage/async_storage.rs | 12 ++++++ .../rust/src/async_storage/mod.rs | 1 + .../rust_builder/rust/src/lib.rs | 40 ++++++++++++++++++- integration_tests/specs/rust/main.ts | 2 +- 6 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 integration_tests/rust_builder/rust/src/async_storage/async_storage.rs create mode 100644 integration_tests/rust_builder/rust/src/async_storage/mod.rs diff --git a/bridge/core/executing_context.cc b/bridge/core/executing_context.cc index fd5478665..32ee8f71f 100644 --- a/bridge/core/executing_context.cc +++ b/bridge/core/executing_context.cc @@ -399,7 +399,9 @@ void ExecutingContext::SetRunRustFutureTasks(const std::shared_ptrprofiler()->StartTrackAsyncEvaluation(); run_rust_future_tasks_->Invoke(this, 0, nullptr); + dart_isolate_context_->profiler()->FinishTrackAsyncEvaluation(); } void ExecutingContext::DrainPendingPromiseJobs() { diff --git a/bridge/core/native/native_loader.cc b/bridge/core/native/native_loader.cc index f7d0eed80..5192e61f6 100644 --- a/bridge/core/native/native_loader.cc +++ b/bridge/core/native/native_loader.cc @@ -40,8 +40,6 @@ static void ExecuteNativeLibrary(PluginLibraryEntryPoint entry_point, native_library_load_context->context->RunRustFutureTasks(); } - native_library_load_context->promise_resolver->Resolve(JS_UNDEFINED); - delete native_library_load_context; } diff --git a/integration_tests/rust_builder/rust/src/async_storage/async_storage.rs b/integration_tests/rust_builder/rust/src/async_storage/async_storage.rs new file mode 100644 index 000000000..5d0c9428d --- /dev/null +++ b/integration_tests/rust_builder/rust/src/async_storage/async_storage.rs @@ -0,0 +1,12 @@ +use webf_sys::ExecutingContext; + +pub async fn test_should_work_with_set_item(context: ExecutingContext) { + let storage = context.async_storage(); + let exception_state = context.create_exception_state(); + + storage.clear(&exception_state).await.unwrap(); + storage.set_item("keyValue", "12345", &exception_state).await.unwrap(); + let value = storage.get_item("keyValue", &exception_state).await.unwrap().unwrap(); + + assert_eq!(value, "12345"); +} diff --git a/integration_tests/rust_builder/rust/src/async_storage/mod.rs b/integration_tests/rust_builder/rust/src/async_storage/mod.rs new file mode 100644 index 000000000..2a114e51d --- /dev/null +++ b/integration_tests/rust_builder/rust/src/async_storage/mod.rs @@ -0,0 +1 @@ +pub mod async_storage; diff --git a/integration_tests/rust_builder/rust/src/lib.rs b/integration_tests/rust_builder/rust/src/lib.rs index f4a8a0d93..697683bdf 100644 --- a/integration_tests/rust_builder/rust/src/lib.rs +++ b/integration_tests/rust_builder/rust/src/lib.rs @@ -1,7 +1,12 @@ +use std::cell::RefCell; use std::ffi::c_void; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; use webf_sys::executing_context::ExecutingContextRustMethods; -use webf_sys::{initialize_webf_api, ExecutingContext, RustValue}; +use webf_sys::{initialize_webf_api, ExecutingContext, FutureRuntime, RustValue}; +pub mod async_storage; pub mod navigator; pub mod storage; @@ -13,9 +18,20 @@ fn webf_test_runner(tests: &[&dyn Fn(ExecutingContext)], context: ExecutingConte } } +type TestFn = Box Pin>>>; + +async fn webf_test_runner_async(tests: &[TestFn], context: ExecutingContext) { + println!("Running {} tests", tests.len()); + for test in tests { + test(context.clone()).await; + println!("Test passed"); + } +} + #[no_mangle] pub extern "C" fn init_webf_test_app(handle: RustValue) -> *mut c_void { - let context = initialize_webf_api(handle); + let context: ExecutingContext = initialize_webf_api(handle); + let context_async = context.clone(); let tests: &[&dyn Fn(ExecutingContext)] = &[ &navigator::navigator::test_user_agent, @@ -26,5 +42,25 @@ pub extern "C" fn init_webf_test_app(handle: RustValue { it('should work with rust native library', async () => { // @ts-expect-error await nativeLoader.loadNativeLibrary('rust_native_api_tests', {}).catch(err => console.log(err)); - }); + }, 10 * 1000); });